封面内页

inside front cover

BDD 团队的主要活动

Key activities of a BDD team

  

  

 

 

 

 

BDD 实际应用

BDD in Action

第二版

Second Edition

 

 

整个软件生命周期的行为驱动开发

Behavior-Driven Development for the whole software lifecycle

 

 

约翰·弗格森·斯马特

John Ferguson Smart

简·莫拉克

Jan Molak

 

 

丹尼尔·特霍斯特-诺斯 (Daniel Terhorst-North) 撰写的前言

Foreword by Daniel Terhorst-North

 

 

要发表评论,请访问liveBook

To comment go to liveBook

 

 

 

 

 

 

有关此书及其他 Manning 作品的更多信息,请访问

For more information on this and other Manning titles go to

www.manning.com

www.manning.com

 

 

对第一版的赞扬

Praise for the first edition

参考资料丰富、深入且广泛,有很多精彩观点。您将找到方法来接近、改进并立即开始熟练使用 BDD 技术。

Good, deep, and wide reference, with many excellent points. You will find ways to approach, improve, and start immediately using BDD techniques proficiently.

—Ferdinando Santacroce,C# 软件开发人员,CompuGroup Medical Italia

—Ferdinando Santacroce, C# software developer, CompuGroup Medical Italia

从上到下学习 BDD,并在读完本书后立即开始使用它。

Learn BDD top to bottom and start using it as soon as you finish reading the book.

—Dror Helper,CodeValue 高级顾问

—Dror Helper, Senior consultant, CodeValue

如果您想以非常实用的方式了解 BDD,那么这本书非常适合您。作者向我们展示了许多有用的技术、工具和概念,它们将帮助我们更有效地使用 BDD。

If you want to see BDD done in a very practical way, this book is for you. The author shows us many useful techniques, tools, and notions that will help us be more productive with BDD.

—Karl Métivier,Facilité Informatique 敏捷教练

—Karl Métivier, Agile coach, Facilité Informatique

BDD 中第一个完整的分步指南。 

The first and complete step-by-step guide in BDD. 

—liquidlabs GmbH 质量保证主管 Marc Bluemner

—Marc Bluemner, head of QA, liquidlabs GmbH

我强烈向所有关注软件质量的同事、学生和软件工程师推荐这本书。

I highly recommend this book to all of my colleagues, students, and software engineers concerned with software quality.

—David Cabrero Souto,拉科鲁尼亚大学 Madsgroup 研究小组主任

—David Cabrero Souto, director of Research Group Madsgroup, University of A Coruña

版权

Copyright

如需在线了解这些书籍和其他 Manning 书籍的信息以及订购它们,请访问www.manning.com。出版商为这些书籍提供批量订购折扣。

For online information and ordering of these  and other Manning books, please visit www.manning.com. The publisher offers discounts on these books when ordered in quantity.

如需了解更多信息,请联系

For more information, please contact

 

 

特约销售部

Special Sales Department

曼宁出版公司

Manning Publications Co.

鲍德温路 20 号

20 Baldwin Road

邮政信箱 761

PO Box 761

纽约州谢尔特岛 11964

Shelter Island, NY 11964

电子邮件: orders@manning.com

Email: orders@manning.com

 

 

 

 

未经出版商事先书面许可,不得以任何形式或通过电子、机械、影印或其他方式复制、存储在检索系统中或传播本出版物的任何部分。

No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or by means electronic, mechanical, photocopying, or otherwise, without prior written permission of the publisher.

制造商和销售商用来区分其产品的许多名称均已声明为商标。这些名称出现在书中,并且 Manning Publications 知道商标声明,这些名称均以首字母大写或全部大写印刷。

Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in the book, and Manning Publications was aware of a trademark claim, the designations have been printed in initial caps or all caps.

认识到保存已写内容的重要性,Manning 的政策是将我们出版的书籍印刷在无酸纸上,并为此尽最大努力。同时认识到我们有责任保护地球资源,Manning 书籍印刷在至少 15% 的回收纸上,并且在加工过程中不使用元素氯。

Recognizing the importance of preserving what has been written, it is Manning’s policy to have the books we publish printed on acid-free paper, and we exert our best efforts to that end. Recognizing also our responsibility to conserve the resources of our planet, Manning books are printed on paper that is at least 15 percent recycled and processed without the use of elemental chlorine.

 

 

    

    

曼宁出版公司

Manning Publications Co.

20 鲍德温路技术

20 Baldwin Road Technical

邮政信箱 761

PO Box 761

纽约州谢尔特岛 11964

Shelter Island, NY 11964

 

 

开发编辑:  

Development editors:  

Helen Stergius 和 Marina Michaels

Helen Stergius and Marina Michaels

技术开发编辑:  

Technical development editor:  

尼克·沃茨

Nick Watts

评论编辑:  

Review editor:  

米哈埃拉·巴蒂尼奇

Mihaela Batinić

制作编辑:  

Production editor:  

凯莉·黑尔斯

Keri Hales

文字编辑:  

Copy editor:  

米歇尔·米切尔

Michele Mitchell

校对:  

Proofreader:  

杰森·埃弗里特

Jason Everett

技术校对:  

Technical proofreader:  

斯里哈里·斯里达兰

Srihari Sridharan

排字员:  

Typesetter:  

丹尼斯·达林尼克

Dennis Dalinnik

封面设计师:  

Cover designer:  

玛丽亚·都铎

Marija Tudor

 

 

国际标准书号:9781617297533

ISBN: 9781617297533

奉献

dedication

刻意的发现——一首“十四行诗”

Deliberate Discovery—A “Sonnet”

不确定性是一切新事物的灵感来源,

而无知是她发挥作用的空间;

一年的时间足以证明一个愿景是正确的,

但我们只需几天就能证明它是错误的。

我们梦想,追逐梦想,从不害怕

失败,一再失败。站起来,站起来!然后继续前进,

但要求我们追求他人的目标

,失败会让我们变成老鼠,而我们却是人。

啊,最好的计划!你最后在哪里?

是谁从一开始就拴住我们、限制我们?

我们知道你交了一个善变、脆弱的朋友;

当你声称你有心时,你欺骗了我们!

我们以为人迹罕至的道路会让我们获胜,

在其他傻瓜害怕误入歧途的地方——

如果我们从一开始就知道

我们一路上发现的无知。然而, 填写、扫描和添加

一份危险和灾难清单, 仍然会遗漏一些我们掌握的东西—— 我们不知道我们以前不知道。 我们根据已制作的地图规划路线 假设地形相同, 希望有平坦的道路让我们保持稳定 当地的生物也都驯服。 我们在商队和马车上装上 好的建议、最佳实践和工具 但没有发现传说——“这里有龙!” 所以我们又一次被打败了。 人们说傻瓜 冲进来,但我们却认为自己很聪明, 我们互相称赞技能,举杯庆祝 智慧——无视 与我们一样熟练的探险队的消亡。 当他们筋疲力尽、自尊心碎地回来时, 我们笑着说:“死亡行军!你希望 这样的事情失败。” 在我们聪明的头脑中 这是显而易见的——至少回想起来是这样。 我们无知的龙会杀死我们 如果我们不先杀死它们。 我们可以勇敢地为国王工作, 当我们因为找到他们的洞穴而耽搁时, 他们不会拒绝付钱给我们。 据说物质无法被创造, 这是基本原理和法则, 而龙却不断出现,势不可挡; 你杀了多少,还会有一条。 我们的无知是无限的——要心存感激, 否则我们会发现我们没有什么可学的了; 被龙吓到可能是命中注定的, 但说实话,最好的计划总是失败的。 我们可以在地牢里找到龙 ,轻轻地走在那里,准备撤退; 我们可以寻找其他道路,推迟走大路,







































































只与我们可能打败的人战斗。

世界可以是一个屠龙者的世界

,以男人和女人而不是老鼠的身份站着;

学习更多知识所带来的快乐应该影响我们;

最凶猛的龙不会让我们再次感到惊讶。

发现小龙,无论它们数量多少,

以及所有最强大的龙,都给予同样的赞美——

不确定性是我们对一切新事物的缪斯,

无知是她玩耍的空间。

Uncertainty’s the muse of all that’s new,

And ignorance the space in which she plays;

A year’s enough to prove a vision true,

But we could prove it false in only days.

We dream, and chase our dream, and never fear

To fail, and fail. Up, up! And on again,

But ask us to pursue another’s goals

And failure makes us mice where we were men.

Ah, best laid plans! Where were you at the end

Who chained us and constrained us from the start?

We knew you made a fickle, fragile friend;

You tricked us when you claimed you had a heart!

We thought less travelled roads would see us winning

In places other fools had feared to stray—

If only we had known from the beginning

The ignorance we found along the way.

And yet, a list of dangers and disasters

Filled out, and scanned, and added to some more

Would still have left out some of what we mastered—

We didn’t know we didn’t know before.



We planned our way with maps we’d made already

Assuming the terrain would be the same,

Expecting well-paved roads to keep us steady

And any local creatures to be tame.

We loaded up our caravans and wagons

With good advice, best practices and tools

But didn’t spot the legend—“Here be dragons!”

So we got burnt, again. They say that fools

Rush in, and yet we count ourselves as wise,

We praise each other’s skill and raise a glass

To intellect—ignoring the demise

Of expeditions just as skilled as ours.

When they return, worn out, their pride in shreds,

We laugh and say, “A death march! You expect

Such things to fail.” And in our clever heads

It’s obvious—at least in retrospect.

The dragons of our ignorance will slay us

If we don’t slay them first. We could be brave

And work for kings who don’t refuse to pay us

When we’re delayed because we found their cave.

They say that matter cannot be created,

A fundamental principle and law,

While dragons keep emerging, unabated;

As many as you slay, there’s still one more.

Our ignorance is limitless—be grateful,

Or else we’d find we’ve nothing left to learn;

To be surprised by dragons may be fateful,

But truth be told, it’s best laid plans that burn.

We could seek out the dragons in their dungeons

And tread there softly, ready to retreat;

We could seek other roads, postponing large ones,

And only fight the ones we might defeat.

The world could be a world of dragon slayers

And stand as men and women, not as mice;

The joy that comes from learning more should sway us;

The fiercest dragons won’t surprise us twice.

Discover tiny dragons, be they few,

And all the mightiest, with equal praise—

Uncertainty’s our muse of all that’s new,

And ignorance the space in which she plays.

—莉兹·基奥 (Liz Keogh)

—Liz Keogh

内容

contents

  

  

前言

Front matter

前言

foreword

前言

preface

致谢

acknowledgments

关于这本书

about this book

关于作者

about the authors

关于封面插图

about the cover illustration

  

  

第 1 部分第一步

Part 1. First steps

  1   构建与众不同的软件

  1   Building software that makes a difference

  1.1   从 50,000 英尺高度进行 BDD

  1.1   BDD from 50,000 feet

  1.2   您要解决什么问题?

  1.2   What problems are you trying to solve?

正确构建软件

Building the software right

构建正确的软件

Building the right software

知识约束:应对不确定性

The knowledge constraint: Dealing with uncertainty

  1.3    BDD 适合您的项目吗?

  1.3   Is BDD right for your projects?

  1.4   您将在本书中学到什么

  1.4   What you will learn in this book

  2   引入行为驱动开发

  2   Introducing Behavior-Driven Development

  2.1    BDD 最初是为了让 TDD 教学更容易而设计的

  2.1   BDD was originally designed to make teaching TDD easier

  2.2    BDD 也适用于需求分析

  2.2   BDD also works well for requirements analysis

  2.3    BDD 原则与实践

  2.3   BDD principles and practices

专注于提供商业价值的功能

Focus on features that deliver business value

共同指定功能

Work together to specify features

拥抱不确定性

Embrace uncertainty

用具体的例子来说明特点

Illustrate features with concrete examples

小黄瓜底漆

A Gherkin primer

不要编写自动化测试;编写可执行规范

Don’t write automated tests; write executable specifications

这些原则也适用于单元测试

These principles also apply to unit tests

提供动态文档

Deliver living documentation

使用动态文档支持正在进行的维护工作

Use living documentation to support ongoing maintenance work

  2.4    BDD 的好处

  2.4   Benefits of BDD

减少浪费

Reduced waste

降低成本

Reduced costs

更轻松、更安全的改变

Easier and safer changes

更快发布

Faster releases

  2.5    BDD 的缺点和潜在挑战

  2.5   Disadvantages and potential challenges of BDD

BDD 需要高度的业务参与和协作

BDD requires high business engagement and collaboration

BDD 在敏捷或迭代环境中效果最佳

BDD works best in an Agile or iterative context

BDD 在孤岛中无法很好地发挥作用

BDD doesn’t work well in a silo

编写不佳的测试可能会导致更高的测试维护成本

Poorly written tests can lead to higher test-maintenance costs

  3   BDD:旋风之旅

  3   BDD: The whirlwind tour

  3.1    BDD 流程

  3.1   The BDD flow

  3.2   推测:确定业务价值和特征

  3.2   Speculate: Identifying business value and features

确定业务目标

Identifying business objectives

发现功能和特性

Discovering capabilities and features

描述特征

Describing features

  3.3   举例说明:通过示例探索特征

  3.3   Illustrate: Exploring a feature with examples

发现功能

Discovering the feature

将功能切分为用户故事

Slicing the feature into User Stories

  3.4   制定:从示例到可执行规范

  3.4   Formulate: From examples to executable specifications

  3.5   自动化:从可执行规范到自动化测试

  3.5   Automate: From executable specifications to automated tests

使用 Maven 和 Cucumber 设置项目

Setting up a project with Maven and Cucumber

在 Cucumber 中记录可执行规范

Recording the executable specifications in Cucumber

自动化可执行规范

Automating the executable specifications

实现粘合代码

Implementing the glue code

  3.6   演示:测试作为动态文档

  3.6   Demonstrate: Tests as living documentation

  3.7    BDD 降低维护成本

  3.7   BDD reduces maintenance costs

第 2 部分。   我想要什么?使用 BDD 定义需求

Part 2.   What do I want? Defining requirements using BDD

  4   推测:从业务目标到优先功能

  4   Speculate: From business goals to prioritized features

  4.1   推测阶段

  4.1   The Speculate phase

BDD 项目中的战略规划

Strategic Planning in a BDD project

战略规划是一项持续的活动

Strategic Planning is a continuous activity

战略规划涉及利益相关者和团队成员

Strategic Planning involves both stakeholders and team members

识别假设和假定,而不是特征

Identifying hypotheses and assumptions rather than features

  4.2   描述业务愿景和目标

  4.2   Describing business vision and goals

愿景、目标、能力和特点

Vision, goals, capabilities, and features

你想实现什么?从愿景开始

What do you want to achieve? Start with a vision

愿景声明

The vision statement

使用愿景声明模板

Using vision statement templates

这将给企业带来什么好处?确定业务目标

How will it benefit the business? Identify the business goals

撰写良好的商业目标

Writing good business goals

给我钱:业务目标和收入

Show me the money: Business goals and revenue

揭开“为什么”的面纱:挖掘业务目标

Popping the “why stack”: Digging out the business goals

  4.3   影响图

  4.3   Impact Mapping

确定痛点

Identify the pain point

定义业务目标

Define the business goal

谁将受益?定义参与者

Who will benefit? Defining the actors

他们的行为应该如何改变?定义影响

How should their behavior change? Defining the impacts

我们应该怎么做?定义可交付成果

What should we do about it? Defining the deliverables

逆向影响映射

Reverse Impact Mapping

  4.4   海盗画布

  4.4   Pirate Canvases

海盗指标

Pirate Metrics

从海盗指标到海盗画布

From Pirate Metrics to Pirate Canvases

发现糟糕的事情

Discovering what sucks

打造壮丽景观

Building the Epic Landscape

  5   描述并确定特征的优先级

  5   Describing and prioritizing features

  5.1    BDD 和产品待办事项细化

  5.1   BDD and Product Backlog Refinement

  5.2   什么是特征?

  5.2   What is a feature?

特性提供能力

Features deliver capabilities

功能可以分解为更易于管理的部分

Features can be broken down into more manageable chunks

一个功能可以通过一个或多个用户故事来描述

A feature can be described by one or more User Stories

功能不是用户故事

A feature is not a User Story

发布特性和产品特性

Release features and product features

并非所有事物都适合等级制度

Not everything fits into a hierarchy

  5.3   真正的选择:不要在必要之前做出承诺

  5.3   Real Options: Don’t make commitments before you have to

选择权有价值

Options have value

期权到期

Options expire

除非知道原因,否则不要过早承诺

Never commit early unless you know why

  5.4   故意发现

  5.4   Deliberate Discovery

  5.5 使用 BDD 进行发布和冲刺规划

  5.5 Release and sprint planning with BDD

  6   举例说明特点

  6   Illustrating features with examples

  6.1   三个朋友和其他需求发现研讨会

  6.1   The Three Amigos and other requirements discovery workshops

  6.2   举例说明特征

  6.2   Illustrating features with examples

  6.3   使用表格描述更复杂的需求

  6.3   Using tables to describe more complex requirements

  6.4   示例映射

  6.4   Example Mapping

示例映射从用户故事开始

Example Mapping starts with a User Story

寻找规则和例子

Finding rules and examples

发现新规则

Discovering new rules

浮现的不确定性

Surfacing uncertainty

促进示例映射会话

Facilitating an Example Mapping session

  6.5   特征映射

  6.5   Feature Mapping

特征映射从一个例子开始

Feature Mapping begins with an example

示例分为几个步骤

Examples are broken into steps

寻找变化和新规则

Look for variations and new rules

寻找替代流程

Look for alternate flows

对相关流量进行分组并记录不确定性

Grouping related flows and recording uncertainty

  6.6   面向对象编程

  6.6   OOPSI

结果

Outcomes

输出

Outputs

过程

Process

场景

Scenarios

输入

Inputs

  7   从示例到可执行规范

  7   From examples to executable specifications

  7.1   将具体示例转化为可执行场景

  7.1   Turning concrete examples into executable scenarios

  7.2   编写可执行场景

  7.2   Writing executable scenarios

功能文件具有标题和描述

A feature file has a title and a description

描述场景

Describing the scenarios

给定...当...然后结构

The Given ... When ... Then structure

和但是

Ands and buts

评论

Comments

  7.3   在场景中使用表格

  7.3   Using tables in scenarios

在单独的步骤中使用表格

Using tables in individual steps

使用示例表

Using tables of examples

待定情景

Pending scenarios

  7.4   使用特性文件和标签组织场景

  7.4   Organizing your scenarios using feature files and tags

场景放在功能文件中

The scenarios go in a feature file

功能文件可以包含一个或多个场景

A feature file can contain one or more scenarios

组织功能文件

Organizing the feature files

使用扁平目录结构

Using a flat directory structure

按故事或产品增量组织功能文件

Organizing feature files by stories or product increments

按功能和能力组织功能文件

Organizing feature files by functionality and capability

使用标签注释你的场景

Annotating your scenarios with tags

提供背景和上下文以避免重复

Provide background and context to avoid duplication

  7.5   规则与示例

  7.5   Rules and examples

  7.6   表达场景:模式和反模式

  7.6   Expressive scenarios: Patterns and anti-patterns

制作美味小黄瓜的艺术

The art of good Gherkin

坏小黄瓜是什么样子的

What bad Gherkin looks like

好的场景是声明性的,而不是命令性的

Good scenarios are declarative, not imperative

好的方案只做一件事,并且做好一件事

Good scenarios do one thing, and one thing well

好的剧本有有意义的演员

Good scenarios have meaningful actors

好的场景聚焦于本质,隐藏次要部分

Good scenarios focus on the essential and hide the incidental

Gherkin 场景不是测试脚本

Gherkin scenarios are not test scripts

好的场景是独立的

Good scenarios are independent

  7.7   但是所有的细节在哪里呢?

  7.7   But where are all the details?

第 3 部分。   我该如何构建它?使用 BDD 方式进行编码

Part 3.   How do I build it? Coding the BDD way

  8   从可执行规范到自动验收测试

  8   From executable specifications to automated acceptance tests

  8.1   自动化场景介绍

  8.1   Introduction to automating scenarios

步骤定义解释步骤

Step definitions interpret the steps

  8.2   设置你的项目

  8.2   Setting up your project

使用 Java 或 TypeScript 设置 Cucumber 项目

Setting up a Cucumber project in Java or TypeScript

使用 Java 组织 Cucumber 项目

Organizing a Cucumber project in Java

使用 TypeScript 组织 Cucumber 项目

Organizing a Cucumber project in TypeScript

  8.3   运行 Cucumber 场景

  8.3   Running Cucumber scenarios

Java 中的 Cucumber 测试运行器类

Cucumber test runner classes in Java

使用 JavaScript 和 TypeScript 运行 Cumber 场景

Running Cucumber scenarios in JavaScript and TypeScript

  8.4   编写粘合代码

  8.4   Writing glue code

使用步骤定义参数注入数据

Injecting data with step definition parameters

让你的 Cucumber 表达式更加灵活

Making your Cucumber Expressions more flexible

Cucumber 表达式和自定义参数类型

Cucumber Expressions and custom parameter types

使用正则表达式

Using regular expressions

使用列表和数据表

Working with lists and data tables

  8.5   使用背景和钩子进行设置和拆除

  8.5   Setting up and tearing down with backgrounds and hooks

使用后台步骤

Using background steps

使用钩子

Using hooks

  8.6   使用钩子准备测试环境

  8.6   Preparing your test environments using hooks

使用内存数据库

Using in-memory databases

  8.7   使用虚拟测试环境

  8.7   Using virtual test environments

使用 TestContainers 管理测试的 Docker 容器

Using TestContainers to manage Docker containers for your tests

  9   编写可靠的自动化验收测试

  9   Writing solid automated acceptance tests

  9.1   编写工业强度的验收测试

  9.1   Writing industrial-strength acceptance tests

  9.2   使用角色和已知实体

  9.2   Using personas and known entities

在场景中使用角色

Working with persona in your scenarios

在 HOCON 中存储人物角色数据

Storing persona data in HOCON

  9.3   抽象层

  9.3   Layers of abstraction

业务规则层描述预期结果

The Business Rules layer describes the expected outcomes

业务流程层描述用户的旅程

The Business Flow layer describes the user’s journey

业务任务与应用程序或其他任务交互

Business tasks interact with the application or with other tasks

技术层与系统交互

The Technical layer interacts with the system

10   UI 层的自动化验收标准

10   Automating acceptance criteria for the UI layer

10.1   何时以及如何测试 UI?

10.1   When and how should you test the UI?

10.2    UI 测试在您的测试自动化策略中处于什么位置?

10.2   Where does UI testing fit in your test automation strategy?

哪些场景应该以 UI 测试的形式实现?

Which scenarios should be implemented as UI tests?

展示用户旅程

Illustrating user journeys

在用户界面中说明业务逻辑

Illustrating business logic in the user interface

记录并验证特定于屏幕的业务逻辑

Documenting and verifying screen-specific business logic

显示信息在用户界面中的呈现方式

Showing how information is rendered in the user interface

使用 Selenium WebDriver 自动化基于 Web 的验收标准

Automating web-based acceptance criteria using Selenium WebDriver

Java 中的 WebDriver 入门

Getting started with WebDriver in Java

设置 WebDriver 驱动程序

Setting up a WebDriver driver

将 WebDriver 与 Cucumber 集成

Integrating WebDriver with Cucumber

在步骤定义类之间共享 WebDriver 实例

Sharing WebDriver instances between step definition classes

与网页交互

Interacting with the web page

如何定位页面上的元素

How to locate elements on a page

与网络元素交互

Interacting with web elements

使用现代 UI 库组件

Working with modern UI library components

使用异步页面并测试 AJAX 应用程序

Working with asynchronous pages and testing AJAX applications

10.3   易于测试的 Web 应用程序

10.3   Test-friendly web applications

10.4   下一步

10.4   Next steps

11   UI层的测试自动化设计模式

11   Test automation design patterns for the UI layer

11.1   非结构化测试脚本的局限性

11.1   The limitations of unstructured test scripts

11.2   将位置逻辑与测试逻辑分离

11.2   Separating location logic from test logic

11.3   介绍页面对象模式

11.3   Introducing the Page Objects pattern

页面对象负责定位页面上的元素

Page Objects are responsible for locating elements on a page

页面对象表示页面上的对象,而不是整个页面

Page Objects represent objects on a page, not an entire page

页面对象告诉你页面的状态

Page Objects tell you about the state of a page

页面对象执行业务任务或模拟用户行为

Page Objects perform business tasks or simulate user behavior

页面对象以业务术语呈现状态

Page Objects present state in business terms

页面对象隐藏等待条件和其他偶然的实现细节

Page Objects hide wait conditions and other incidental implementation details

页面对象不包含断言

Page Objects do not contain assertions

WebDriver 页面工厂和 @FindBy 注释

WebDriver Page Factories and the @FindBy annotation

查找集合

Finding collections

Serenity BDD 中的页面对象

Page Objects in Serenity BDD

11.4   超越页面对象

11.4   Going beyond Page Objects

动作类

Action classes

查询类

Query classes

DSL 层和构建器

DSL layers and builders

12   使用 Screenplay 模式实现可扩展的测试自动化

12   Scalable test automation with the Screenplay Pattern

12.1   什么是剧本模式?为什么我们需要它?

12.1   What is the Screenplay Pattern, and why do we need it?

12.2   剧本基础

12.2   Screenplay fundamentals

12.3   什么是演员?

12.3   What is an actor?

12.4   参与者执行任务

12.4   Actors perform tasks

12.5   交互模型描述了参与者如何与系统交互

12.5   Interactions model how actors interact with the system

演员可以进行多种互动

Actors can perform multiple interactions

交互是对象,而不是方法

Interactions are objects, not methods

交互可以执行等待以及操作

Interactions can perform waits as well as actions

Interactions 也可以与 REST API 进行交互

Interactions can also interact with REST APIs

12.6   能力是参与者与系统交互的方式

12.6   Abilities are how actors interact with the system

12.7   编写我们自己的交互类

12.7   Writing our own interaction classes

12.8   问题允许参与者查询系统的状态

12.8   Questions allow an actor to query the state of the system

询问系统状态的问题

Questions query the state of the system

特定领域的问题类使我们的代码更具可读性

Domain-specific Question classes make our code more readable

演员可以用问题来做出断言

Actors can use questions to make assertions

12.9   任务模型化更高级别的业务操作

12.9   Tasks model higher-level business actions

简单任务提高可读性

Simple tasks improve readability

更复杂的任务可增强可重用性

More complex tasks enhance reusability

12.10 剧本和黄瓜

12.10 Screenplay and Cucumber

演员和剧组

Actors and casts

剧本阶段

The Screenplay stage

为参与者定义自定义参数类型

Defining a custom parameter type for actors

在枚举值中定义角色

Defining persona in enum values

Cucumber 中的剧本断言

Screenplay assertions in Cucumber

十三   微服务和 API 的 BDD 和可执行规范

13   BDD and executable specifications for microservices and APIs

13.1    API 及其测试方法

13.1   APIs and how to test them

13.2   使用 Web UI 和微服务定义功能

13.2   Defining a feature using a web UI and a microservice

了解要求

Understanding the requirements

从需求到可执行规范

From requirements to executable specifications

13.3   微服务自动化验收测试

13.3   Automating acceptance tests for microservices

13.4   正在测试的微服务架构

13.4   The microservice architecture under test

准备测试数据

Preparing the test data

执行 POST 查询:注册飞行常客会员

Performing a POST query: Registering a Frequent Flyer member

使用 JSONPath 查询 JSON 响应

Querying JSON responses with JSONPath

执行 GET 查询:确认常旅客地址

Performing a GET query: Confirming the frequent flyer address

部分 JSON 响应:检查新的飞行常客帐户详细信息

Partial JSON Responses: Checking the new Frequent Flyer account details

执行 DELETE 查询:事后清理

Performing a DELETE query: Cleaning up afterward

13.5   自动化更细粒度的场景并与外部服务交互

13.5   Automating more granular scenarios and interacting with external services

13.6   测试 API 或使用 API 进行测试

13.6   Testing the APIs or testing with the APIs

14   使用 Serenity/JS 为现有系统提供可执行规范

14   Executable specifications for existing systems with Serenity/JS

14.1   使用旅程地图探索未知领域

14.1   Navigating an uncharted territory with Journey Mapping

确定参与者和目标以了解业务环境

Determine actors and goals to understand the business context

确定哪些工作流程支持感兴趣的目标

Determine what workflows support the goals of interest

将工作流与功能关联

Associate workflows with features

建立一系列场景来展示功能

Establish a steel thread of scenarios that demonstrate the features

确定每种情景的可验证后果

Determine verifiable consequences of each scenario

使用任务分析来理解每个场景的步骤

Using task analysis to understand the steps of each scenario

14.2   设计可扩展的测试自动化系统

14.2   Designing scalable test automation systems

使用分层架构设计可扩展的测试自动化系统

Using layered architecture to design scalable test automation systems

使用参与者链接测试自动化系统的各个层

Using actors to link the layers of a test automation system

使用演员来描述角色

Using actors to describe personas

14.3   在规范层捕获业务上下文

14.3   Capturing business context in the Specification layer

15   使用 Serenity/JS 进行可移植测试自动化

15   Portable test automation with Serenity/JS

15.1   设计测试自动化系统的领域层

15.1   Designing the Domain layer of a test automation system

建模业务领域任务

Modeling business domain tasks

实施业务领域任务

Implementing business domain tasks

将交互组合成任务

Composing interactions into tasks

使用由外而内的方法实现任务替代

Using an outside-in approach to enable task substitution

利用混合测试实现非 UI 交互

Leveraging non-UI interactions with blended testing

使用任务作为代码重用的机制

Using tasks as a mechanism for code reuse

执行验证任务

Implementing verification tasks

15.2   设计可移植的集成层

15.2   Designing a portable Integration layer

为 Web 界面编写可移植测试

Writing portable tests for the web interfaces

识别页面元素

Identifying page elements

实现精益页面对象

Implementing Lean Page Objects

实现配套页面对象

Implementing Companion Page Objects

使用页面元素实现可移植的交互

Implementing portable interactions with Page Elements

使用页面元素查询语言描述复杂的 UI 小部件

Using Page Element Query Language to describe complex UI widgets

配置 Web 集成工具

Configuring web integration tools

跨项目和团队共享测试代码

Sharing test code across projects and teams

16   生活记录和发布证据

16   Living documentation and release evidence

16.1   动态文档:高层视图

16.1   Living documentation: A high-level view

16.2   报告功能准备情况和功能覆盖率

16.2   Reporting on feature readiness and feature coverage

功能准备情况:哪些功能已准备好交付

Feature readiness: What features are ready to deliver

功能覆盖范围:已构建哪些要求

Feature coverage: What requirements have been built

16.3   整合数字产品待办事项

16.3   Integrating a digital product backlog

16.4   利用产品待办事项工具实现更好的协作

16.4   Leveraging product backlog tools for better collaboration

16.5   组织动态文档

16.5   Organizing the living documentation

根据高级需求组织动态文档

Organizing living documentation by high-level requirements

使用标签组织动态文档

Organizing living documentation using tags

发布报告的动态文档

Living documentation for release reporting

低级生活文档

Low-level living documentation

单元测试作为动态文档

Unit tests as living documentation

16.6   遗留应用程序的动态文档

16.6   Living documentation for legacy applications

  

  

指数

index

前页

front matter

前言

foreword

欢迎阅读 John Ferguson Smart 的《BDD in Action》第二版。2014 年,当我为第一版撰写前言时,我感到既欣慰又高兴,因为有人承担了捕捉 BDD 方法、工具和技术概况的艰巨任务。John 的方法是仔细、周到、彻底地记录他作为从业者、教练、顾问和培训师所见所闻,我很高兴能为这本书撰写前言,向全世界介绍它。

Welcome to the second edition of John Ferguson Smart’s comprehensive BDD in Action. When I wrote the foreword to the first edition in 2014, it was with a mixture of relief and delight that someone had taken on the mammoth task of capturing the landscape of BDD methods, tools, and techniques. John’s approach was to carefully, thoughtfully, and thoroughly document what he saw and experienced, as a practitioner, coach, consultant, and trainer, and I was excited to write the foreword that introduced this book to the world.

快进到 2023 年,我们生活在一个一切都发生变化的世界。全球疫情使“前所未有”一词的使用率空前高涨。团队和组织正在采用分布式和混合式工作模式,这使得知识工作上的协作比以往任何时候都更加重要,也更具挑战性。

Fast-forward to 2023, and we are living in a world in which everything has changed. A global pandemic has seen an unprecedented increase in the use of the word “unprecedented.” Teams and organizations are adopting distributed and hybrid working patterns, making collaboration on knowledge work simultaneously more important and more challenging than ever before.

行为驱动开发似乎非常适合这个新世界。它专注于整个产品开发周期的沟通,这意味着我们拥有实时文档,作为团队成员和其他利益相关者之间共享的工件,这些利益相关者被地理和时区分开。团队可以就一项功能达成一致,以场景标题的形式讨论其范围(“场景中...”),并深入了解验收标准的细节,同时自动确保他们所同意的内容确实是应用程序的行为方式。将其与您的版本控制和实时路径结合起来,您就已走上了完全持续合规的道路!

Behavior-Driven Development seems to be a perfect fit for this new world. Its focus on communication across the entire product development cycle means we have living documentation as shared artifacts between team members and other stakeholders separated by geography and time zones. The team can agree on a feature, discuss its scope in the form of scenario titles (“The one where ...”), and get into the detail of acceptance criteria, while having automated assurance that what they agreed is indeed how the application behaves. Tie this into your version control and path to live and you are well on the way to full continuous compliance!

在本书的更新版本中,John 和 Jan 重新审视了所有现有内容,提高了初次读者和重读者的清晰度和流畅度。但世界并非停滞不前;自 2014 年以来,BDD 领域出现了多项令人兴奋的新发展。

In this updated edition of the book, John and Jan have revisited all the existing content, improving its clarity and flow for both the first-time reader and the returning practitioner. But the world does not stand still; since 2014 there have been several exciting new developments in the world of BDD.

示例映射是一种简单但功能强大的方法,可用于探索功能、发现不确定性以及捕获假设、业务规则、问题,当然还有示例。我承认,当第一次有人向我描述示例映射时,我的反应是礼貌但困惑的“那又怎么样?”它似乎太简单了,没什么用。但后来许多最好的想法都是如此,从那时起,它就成了我的 BDD 工具包中不可或缺的一部分。

Example Mapping is a simple yet powerful way of exploring a feature, surfacing uncertainty, and capturing assumptions, business rules, questions, and, of course, examples. I will admit that when it was first described to me, I reacted with a polite but confused “So what?” It seemed too simple to be useful. But then many of the best ideas are, and it has since become a staple of my BDD tool kit.

Screenplay 模式是另一种显而易见的技术。大多数 UI 自动化框架都使用 UI 语言(按钮、字段、表单等),即页面模型。Screenplay 颠覆了这种模式,并说:“为什么不像我们在其他地方一样,用业务领域的语言来描述 UI 交互呢?”你不会再回头了。

The Screenplay Pattern is another obvious-when-you-say-it-out-loud technique. Most UI automation frameworks use the language of the UI—buttons, fields, forms, and so forth—known as a page model. Screenplay flips this on its head and says, “Why not describe UI interactions in the language of the business domain, like we do everywhere else?” You won’t go back.

John 和 Jan 以他们惯有的清晰和详细的方式描述了这些和其他有价值的技术,并提供了实际示例,引导读者不仅了解理论,还了解实际的实践。我发现自己对这些新材料点头称是,并且我自己也有一些顿悟的时刻

John and Jan describe these and other valuable techniques with their customary clarity and detail, providing worked examples that guide the reader through not only theory but tangible practice. I found myself nodding along with much of this new material, as well as having a couple of Aha! moments myself.

我很高兴 BDD 在近 20 年后仍然具有如此大的吸引力和兴趣(!),我感谢 John 和 Jan 制作了如此全面的资源的第二版。

I am delighted that BDD still has this much traction and interest nearly 20 years on (!), and I am grateful to John and Jan for producing this second edition of such a comprehensive resource.

—Daniel Terhorst-North,从业顾问

—Daniel Terhorst-North, practitioner consultant

前言

preface

与许多项目一样,当我在 2019 年开始编写《BDD in Action》的新版本时,我认为这会很容易:这里或那里进行一些库更新,也许还会添加几个关于最新需求发现实践的新部分。

Like many projects, when I started working on a new edition to BDD in Action in 2019, I thought it would be easy: a few library updates here and there, and maybe a couple of new sections on the more recent requirement discovery practices.

但当我重读我在 2013-2014 年撰写的有关 BDD 的材料时,我意识到很多事情都发生了变化。套用《大白鲨》中 Roy Scheider 的角色的话,我需要一本更大的书。核心原则保持不变,因为 BDD 背后的基本思想仍然像以往一样坚实和有用。

But as I reread the material I’d written about BDD in 2013–2014, I realized that a lot of things had evolved. To paraphrase Roy Scheider’s character in Jaws, I was going to need a bigger book. The core tenets remained the same, as the fundamental ideas behind BDD are still as solid, and as useful, as ever.

但是我们进行 BDD 的方式已经发生了很大变化。我们已经学会了如何更有效地促进需求发现会议,使用诸如示例映射、功能映射和旅程映射等技术。我们还看到很多团队误解和误解了 BDD 并因此遭受损失,因此澄清一些核心原则似乎很有用。2015 年,我第一次接触到 Screenplay 模式,对我来说,这改变了我编写更干净、更易于维护的测试自动化代码的方式。

But the way we do BDD has evolved quite a bit. We’ve learned how to facilitate requirements discovery sessions more effectively, with techniques such as Example Mapping, Feature Mapping, and Journey Mapping. We have also seen so many teams misinterpret and misunderstand BDD and suffer as a consequence, so some clarification of the core principles seemed useful. In 2015, I was introduced to the Screenplay Pattern for the first time, and for me this was a game-changer in writing cleaner, more maintainable test automation code.

自第一版以来,JavaScript 已经有了巨大的发展;我与Serenity/JS(Screenplay 模式的 TypeScript 实现)的作者 Jan Molak 合作,深入研究如何在 JavaScript 和 TypeScript 项目中实践 BDD 的技术方面。

JavaScript has grown massively since the first edition; I’ve teamed up with Jan Molak, author of Serenity/JS (the TypeScript implementation of the Screenplay pattern), to take a deeper look at how to practice the technical side of BDD in JavaScript and TypeScript projects.

在此版本中,没有一章保持不变,许多章节被完全重写,并且有几个全新的章节。尽情享受吧!

In this edition, no chapter remained untouched, many chapters were completely rewritten, and there are several entirely new ones. Enjoy!

致谢

acknowledgments

就像一部电影一样,一本书有数百名演员,从小角色到为本书做出贡献的人。我们要感谢 Manning 所有员工的奉献、专业精神和对细节的关注:Michael Stephens、Melissa Ice、Rebecca Rinehart、Paul Spratley、Eleonor Gardner 等。我们的开发编辑 Helen Stergius 和 Marina Michaels 一直坚持不懈、彬彬有礼且乐于助人——直到最后一章——推动本书投入生产。感谢 Nick Watts 和 Srihari Sridharan,他们作为技术校对员做得非常出色,并在此过程中提出了一些很棒的建议。

A little like a film, a book has a cast of hundreds, from minor roles to people whose contributions made this book possible. Our thanks go to the dedication, professionalism, and attention to detail of all the folks at Manning: Michael Stephens, Melissa Ice, Rebecca Rinehart, Paul Spratley, Eleonor Gardner, and many others. Helen Stergius and Marina Michaels, our development editors, were unflagging, courteous, and helpful—all the way to the final chapter—in the drive to push this book into production. Thanks to Nick Watts and Srihari Sridharan, who did an exemplary job as technical proofreaders and came up with some great suggestions along the way.

评论者也值得特别提及——没有他们的帮助,本书就不会有今天的成就:Alain Couniot、Alessandro Campeis、Alex Lucas、Andy Wiesendanger、Burk Hufnagel、Christian Kreutzer-Beck、Conor Redmond、Craig Smith、David Paccoud、Goetz Heller、Hilde Van Gysel、Jared Duncan、Jean-François Morin、Jeff Smith、John Booth、John Guthrie、John Kasiewicz、Julien Pohie、Kelum Prabath Senanayake、Kelvin Johnson、Kevin Liao、Lorenzo De Leon、Phillip Sorensen、Richard Vaughan、Ronald Borman、Santosh Shanbhag 和 Viorel Moisei。

The reviewers also deserve special mention—this book would not be what it is without their help: Alain Couniot, Alessandro Campeis, Alex Lucas, Andy Wiesendanger, Burk Hufnagel, Christian Kreutzer-Beck, Conor Redmond, Craig Smith, David Paccoud, Goetz Heller, Hilde Van Gysel, Jared Duncan, Jean-François Morin, Jeff Smith, John Booth, John Guthrie, John Kasiewicz, Julien Pohie, Kelum Prabath Senanayake, Kelvin Johnson, Kevin Liao, Lorenzo De Leon, Phillip Sorensen, Richard Vaughan, Ronald Borman, Santosh Shanbhag, and Viorel Moisei.

我们对 BDD 的了解很大程度上要归功于 BDD 社区:Gojko Adzic、Nigel Charman、Andrew Glover、Liz Keogh、Chris Matts、Dan North、Richard Vowles 等许多人,更不用说更广泛的敏捷和开源社区:Dan Allen、John Hurst、Paul King、Aslak Knutsen、Bartosz Majsak、Gáspár Nagy、Seb Rose、Alex Soto、Renee Troughton、Matt Wynne 等。感谢你们进行了如此多富有成效的对话、电子邮件交流、结对编码会议和 Skype 聊天!特别感谢 Daniel Terhorst-North 为本书撰写序言。

We owe much of what we know about BDD to the BDD community: Gojko Adzic, Nigel Charman, Andrew Glover, Liz Keogh, Chris Matts, Dan North, Richard Vowles, and many others—not to mention the broader Agile and open source communities: Dan Allen, John Hurst, Paul King, Aslak Knutsen, Bartosz Majsak, Gáspár Nagy, Seb Rose, Alex Soto, Renee Troughton, Matt Wynne, and more. Thanks for so many fruitful conversations, email exchanges, pair coding sessions, and Skype chats! Special thanks to Daniel Terhorst-North for contributing the foreword to the book.

我们还要特别感谢 Antony Marcano 和 Andy Palmer,他们向我们介绍了 Screenplay 模式的概念,并帮助我们将自动化编码提升到了一个新的水平。

A special thanks also goes to Antony Marcano and Andy Palmer, who introduced us to the idea of the Screenplay Pattern and helped to take our automation coding to another level.

本书的大部分内容受到多年来与不同组织的客户、朋友和同事所做的工作和对话的启发:Anthony O'Brien、Parikshit Basrur、Tom Howard、Ray King、Ian Mansell、Peter Merel、Michael Rembach、Simeon Ross、Tim Ryan、Tong Su、Peter Suggitt、Marco Tedone、Peter Thomas、Trevor Vella、Gordon Weir、John Singh 等。

Much of the content of the book is inspired by work done and conversations held over the years with clients, friends, and colleagues in many different organizations: Anthony O’Brien, Parikshit Basrur, Tom Howard, Ray King, Ian Mansell, Peter Merel, Michael Rembach, Simeon Ross, Tim Ryan, Tong Su, Peter Suggitt, Marco Tedone, Peter Thomas, Trevor Vella, Gordon Weir, John Singh, and many others.

来自约翰:特别感谢我忠诚的配偶香特尔 (Chantal) 以及我的儿子詹姆斯 (James) 和威廉 (William),如果没有他们的耐心、毅力、支持和鼓励,这本书根本不可能问世。

From John: A very special thanks to my dedicated spouse, Chantal, and my boys, James and William, without whose patience, endurance, support, and encouragement this book would simply not have been possible.

来自 Jan:我要感谢我了不起的妻子安娜,感谢她的爱、支持和耐心。她对这本书的完成和我一样重要。我还要感谢我的两个可爱的女儿亚历山德拉和维多利亚,感谢她们的鼓励和对父亲的信任。

From Jan: I want to thank my amazing wife, Anna, for her love, support, and patience. She was as important to this book getting done as I was. I also want to thank my two wonderful daughters, Alexandra and Victoria, for their encouragement and for believing in their dad.

关于这本书

about this book

本书的目标是帮助团队采用有效的 BDD 实践。它旨在让您全面了解 BDD 实践如何应用于软件开发过程的各个层面,包括发现和定义高级需求、实现应用程序功能以及以自动验收和单元测试的形式编写可执行规范。

The goal of this book is to help get teams up and running with effective BDD practices. It aims to give you a complete picture of how BDD practices apply at all levels of the software development process, including discovering and defining high-level requirements, implementing the application features, and writing executable specifications in the form of automated acceptance and unit tests.

谁应该读这本书

Who should read this book

本书的读者群非常广泛。它既适合完全不熟悉 BDD 的团队,也适合已经尝试实施 BDD 或相关实践(如验收测试驱动开发或示例规范)的团队。它适合那些因需求不一致和变化、因缺陷和返工而浪费时间以及产品质量不佳而苦苦挣扎的团队。它适合那些致力于帮助这些团队的从业者,也适合所有热衷于发现构建和交付软件的更好方法的人。

This book has a broad audience. It’s aimed both at teams who are completely new to BDD and at teams who are already trying to roll out BDD or related practices, like acceptance test–driven development or specification by example. It’s for teams who struggle with misaligned and changing requirements, time wasted due to defects and rework, and product quality. It’s for practitioners whose job is to help these teams, and it’s for everyone who shares a passion for discovering better ways to build and deliver software.

不同的人会从这本书中获得不同的东西:

Different people will get different things out of this book:

  • 业务分析师和测试人员将学习更有效的方法与用户合作发现需求,并将这些需求传达给开发团队。

  • Business analysts and testers will learn more effective ways of discovering requirements in collaboration with users, and of communicating these requirements to development teams.

  • 开发人员将学习如何编写质量更高、更易于维护且错误更少的代码,如何专注于编写提供真正价值的代码,以及如何构建为整个团队提供文档和反馈的自动化测试套件。

  • Developers will learn how to write higher-quality, more maintainable code with fewer bugs, how to focus on writing code that delivers real value, and how to build automated test suites that provide documentation and feedback for the whole team.

  • 项目经理和业务利益相关者将学习如何帮助团队为企业构建更好、更有价值的软件。

  • Project managers and business stakeholders will learn how to help teams build better, more valuable software for the business.

本书的组织结构:路线图

How the book is organized: A road map

本书分为四个部分,每个部分涉及 BDD 的不同方面:

The book is divided into four parts, each addressing different aspects of BDD:

  • 第 1 部分介绍了 BDD 的动机、起源和一般理念,最后简要、实用地介绍了 BDD 在现实世界中的样子。本部分将帮助团队成员和项目利益相关者对 BDD 的真正含义有一个扎实的了解。

  • Part 1 presents the motivations, origins, and general philosophy of BDD and concludes with a quick, practical introduction to what BDD looks like in the real world. This part will help team members and project stakeholders alike get a solid understanding of what BDD is really about.

  • 第 2 部分重点介绍协作。您将在其中了解 BDD 实践如何帮助团队更有效地分析需求,以发现和描述哪些功能将为组织带来真正的价值。本节为本书的其余部分奠定了概念基础,并介绍了一些重要的需求分析技术。

  • Part 2 focuses on collaboration. In it, you will learn how BDD practices can help teams analyze requirements more effectively in order to discover and describe what features will deliver real value to the organization. This section lays the conceptual foundation for the rest of the book and presents a number of important requirements-analysis techniques.

  • 第 3 部分提供了更多关于 BDD 实践的技术介绍。我们将研究以稳健且可持续的方式自动化验收测试的技术,研究适用于不同语言和框架的多种 BDD 工具,并了解 BDD 如何帮助开发人员编写更简洁、设计更好、质量更高的代码。本节内容实用。第 3 部分的最后一章略有不同,它从项目管理、产品文档、报告和集成到构建过程的背景下更广泛地介绍了 BDD。

  • Part 3 provides more technical coverage of BDD practices. We’ll look at techniques for automating acceptance tests in a robust and sustainable way, study a number of BDD tools for different languages and frameworks, and see how BDD helps developers write cleaner, better-designed, higher-quality code. This section is hands-on and practical. The last chapter of part 3 is a little different and takes a look at the broader picture of BDD in the context of project management, product documentation, reporting, and integration into the build process.

书中的大多数实际示例将使用基于 Java 的语言和工具,但我们也会查看 JavaScript 和 TypeScript 的 BDD 工具示例。我们讨论的方法通常适用于任何语言。

Most of the practical examples in the book will use Java-based languages and tools, but we’ll also look at examples of BDD tools for JavaScript and TypeScript. The approaches we discuss will be generally applicable to any language.

由于本书的关注点广泛,您可能会发现不同的章节或多或少适用于您的日常工作。例如,业务分析师可能会发现有关需求分析的材料比有关编码实践的章节更相关。表 1 提供了各种读者可能认为特别有用的章节的(非常)粗略指南。

Because of the broad focus of the book, you may find different sections more or less applicable to your daily work. For example, business analysts might find the material on requirements analysis more relevant than the chapters on coding practices. Table 1 presents a (very) rough guide to the sections various readers might find particularly useful.

表 1. 本书各部分目标读者的粗略指标

Table 1. A rough indicator of the target audience for each section of this book

 

 

商业分析师

Business analyst

测试员

Tester

开发人员

Developer

专案经理

Project manager

第 1 部分。

Part 1.

✅✅✅

✅✅✅

✅✅✅

✅✅✅

第 2 部分。

Part 2.

✅✅✅

✅✅✅

✅✅

✅✅✅

第三部分(第 11 至 15 章)

Part 3. (Chapters 11–15)

 

 

✅✅✅

✅✅✅

 

 

第 3 部分(第 16 章)

Part 3. (Chapter 16)

✅✅✅

✅✅✅

✅✅

✅✅

先决条件

Prerequisites

《BDD in Action》的先决条件将根据所阅读书籍的部分内容而有所不同:

The prerequisites for BDD in Action will vary depending on the parts of the book being read:

  • 第 1 和第 2 部分(高级 BDD) ——这些部分几乎不需要技术知识;它们面向所有团队成员,并介绍 BDD 的一般原则。对敏捷开发实践有基本的了解将会有所帮助。

  • Parts 1 and 2 (high-level BDD)—These sections require little technical knowledge; they are aimed at all team members and introduce general principles of BDD. A basic understanding of Agile development practices will be helpful.

  • 第 3 部分(BDD 和测试自动化) —本部分需要编程知识。大多数示例都使用 Java 或 JavaScript。一般方法是用工作代码来说明概念和实践,而不是详尽地记录任何一种技术。不同的技术部分将受益于以下技术的工作知识:

    • Maven—Java/JVM 代码示例使用 Maven,但只需要一些表面知识(构建 Maven 项目的能力)。
    • HTML/CSS——使用 Selenium/WebDriver 的 UI 测试部分需要对 HTML 页面的构建方式、CSS 选择器的样子有基本的了解,并且(可选)熟悉 XPath。
    • Restful web 服务——有关测试 web 服务的部分需要了解 web 服务的实现方式,特别是 web 服务客户端的实现方式。
    • JavaScript——测试 JavaScript 和 JavaScript 应用程序部分需要对 JavaScript 编程有合理的了解。
  • Part 3 (BDD and test automation)—This section requires programming knowledge. Most of the examples use either Java or JavaScript. The general approach is to illustrate concepts and practices with working code rather than to document any one technology exhaustively. Different technology sections will benefit from a working knowledge of the following technologies:

    • Maven—The Java/JVM code samples use Maven, though only a superficial knowledge (the ability to build a Maven project) is required.
    • HTML/CSS—The sections on UI testing that use Selenium/WebDriver need a basic understanding of how HTML pages are built, what a CSS selector looks like, and, optionally, some familiarity with XPath.
    • Restful web services—The sections on testing web services need some understanding of how web services are implemented, in particular how web service clients are implemented.
    • JavaScript—The section on testing JavaScript and JavaScript applications requires a reasonable understanding of JavaScript programming.
  • 第 16 章(动态文档) ——本节是通用的,没有真正的技术要求。

  • Chapter 16 (living documentation)—This section is general and has no real technical requirements.

关于代码

About the code

本书包含许多源代码示例,用于说明所讨论的各种工具和技术。清单或文本中的源代码出现在 中fixed-width font like this。文本中出现的其他相关概念(例如类或变量名称)也出现在此字体中。

This book contains many source code examples that illustrate the various tools and techniques discussed. Source code in listings or in the text appears in a fixed-width font like this. Other related concepts that appear in the text, such as class or variable names, also appear in this font.

由于本书讨论了许多语言,我们特别努力使所有清单都易于阅读和理解,即使您不熟悉所使用的语言也是如此。大多数清单都带有注释,使代码更易于理解,有些清单还带有编号的提示球,指示下文中讨论的特定代码行。

Because this book discusses many languages, we’ve made a special effort to keep all of the listings readable and easy to follow, even if you’re not familiar with the language being used. Most of the listings are annotated to make the code easier to follow, and some also have numbered cue balls indicating particular lines of code that are discussed in the text that follows.

源代码和其他资源

Source code and other resources

本书包含多种语言的源代码示例。这些示例的源代码可在 GitHub 上下载,网址为https://github.com/bdd-in-action/second-edition,每章都有一个单独的子目录。有些示例跨几章讨论 - 在这种情况下,每章都包含该章讨论的源代码版本。该网站还包含本书中使用的工具和库以及其他有用的相关资源的链接。

This book contains many source code examples in a variety of languages. The source code for these examples is available for download on GitHub at https://github.com/bdd-in-action/second-edition, with a separate subdirectory for each chapter. Some examples are discussed across several chapters—in these cases, each chapter contains the version of the source code discussed in that chapter. This site also contains links to the tools and libraries used in the book and other useful related resources.

此外,您还可以从本书的 liveBook(在线)版本https://livebook.manning.com/book/bdd-in-action-second-edition获取可执行代码片段。代码项目不包含特定于 IDE 的项目文件,但它们的布局方式使其易于导入 IDE。

In addition, you can get executable snippets of code from the liveBook (online) version of this book at https://livebook.manning.com/book/bdd-in-action-second-edition. The code projects don’t contain IDE-specific project files, but they’re laid out in such a way as to make it easy to import them into an IDE.

liveBook 讨论论坛

liveBook discussion forum

购买《BDD in Action》第二版可免费访问 Manning 的在线阅读平台 liveBook。使用 liveBook 独有的讨论功能,您可以对本书全局或特定章节或段落添加评论。您可以轻而易举地为自己做笔记、提出和回答技术问题以及获得作者和其他用户的帮助。要访问论坛,请访问https://livebook.manning.com/book/bdd-in-action-second-edition/discussion 。您还可以在https://livebook.manning.com/discussion上了解有关 Manning 论坛和行为规则的更多信息。

Purchase of BDD in Action, Second Edition includes free access to liveBook, Manning’s online reading platform. Using liveBook’s exclusive discussion features, you can attach comments to the book globally or to specific sections or paragraphs. It’s a snap to make notes for yourself, ask and answer technical questions, and receive help from the author and other users. To access the forum, go to https://livebook.manning.com/book/bdd-in-action-second-edition/discussion. You can also learn more about Manning’s forums and the rules of conduct at https://livebook.manning.com/discussion.

Manning 对我们读者的承诺是提供一个平台,让读者之间以及读者和作者之间可以进行有意义的对话。这并不是对作者参与程度的任何承诺,他们对论坛的贡献仍然是自愿的(并且是无偿的)。我们建议您尝试向作者提出一些有挑战性的问题,以免他们失去兴趣!只要这本书还在印刷中,就可以从出版商的网站上访问论坛和以前讨论的档案。

Manning’s commitment to our readers is to provide a venue where a meaningful dialogue between individual readers and between readers and the authors can take place. It is not a commitment to any specific amount of participation on the part of the authors, whose contribution to the forum remains voluntary (and unpaid). We suggest you try asking the authors some challenging questions lest their interest stray! The forum and the archives of previous discussions will be accessible from the publisher’s website as long as the book is in print.

关于作者

about the authors

John Ferguson Smart是一位国际演讲家、顾问、作家和培训师,他因撰写的多部书籍、文章和演示文稿而闻名于敏捷社区,尤其涉及 BDD、TDD、测试自动化、软件工艺和团队协作等领域。

John Ferguson Smart is an international speaker, consultant, author, and trainer well known in the Agile community for his many books, articles, and presentations, particularly in areas such as BDD, TDD, test automation, software craftsmanship, and team collaboration.

John 的主要工作重点是帮助组织和团队通过结合有效的协作和卓越的技术创造更多价值。John 还是创新的 Serenity BDD 测试自动化库的创建者和首席开发人员,以及 Serenity Dojo ( https://www.serenity-dojo.com/ ) 的创始人,这是一所在线培训和辅导学校,旨在帮助来自各个背景的测试人员成为高效的敏捷测试自动化工程师。

John’s main focus is helping organizations and teams deliver more value by combining effective collaboration and technical excellence. John is also the creator and lead developer of the innovative Serenity BDD test automation library and founder of Serenity Dojo (https://www.serenity-dojo.com/), an online training and coaching school that helps testers from all backgrounds become high-performing Agile test automation engineers.

Jan Molak是一名培训师、演讲者和顾问,通过引入 BDD、先进的测试自动化和现代软件工程实践帮助世界各地的客户改善协作并优化软件交付流程。

Jan Molak is a trainer, speaker, and consultant helping clients around the world improve collaboration and optimize software delivery processes through the introduction of BDD, advanced test automation, and modern software engineering practices.

Jan 是 Screenplay Pattern 的贡献者和多产的开源开发者,同时还是 Serenity/JS 验收测试框架、Jenkins Build Monitor 以及持续交付和测试领域的众多其他工具的作者。

A contributor to the Screenplay Pattern and a prolific open source developer, Jan is also the author of the Serenity/JS acceptance testing framework, Jenkins Build Monitor, and numerous other tools in the continuous delivery and testing space.

关于封面插图

about the cover illustration

《BDD in Action》第二版封面上的人物标题为“中世纪骑士”。插图由 Paolo Mercuri (1804-1884) 绘制,取自卡米尔·博纳尔 (Camille Bonnard) 编辑并于 19 世纪中期在巴黎出版的一本书。

The figure on the cover of BDD in Action, Second Edition is captioned “A Medieval Knight.” The illustration by Paolo Mercuri (1804 –1884) is taken from a book edited by Camille Bonnard and published in Paris in the mid-1800s.

在那个年代,人们很容易通过衣着辨别出他们住在哪里、从事什么行业或社会地位。曼宁以几个世纪前丰富多样的地域文化为基础,通过收藏中的图片,将这种文化重新带回人们的生活,以此来赞美计算机行业的创造力和创新精神。

In those days, it was easy to identify where people lived and what their trade or station in life was just by their dress. Manning celebrates the inventiveness and initiative of the computer business with book covers based on the rich diversity of regional culture centuries ago, brought back to life by pictures from collections such as this one.

第 1 部分 第一步

Part 1. First steps

欢迎来到行为驱动开发 (BDD) 的世界!本书第 1 部分将为您提供 BDD 世界的高层次视角,并让您初步了解 BDD 在该领域的应用。

Welcome to the world of Behavior-Driven Development (BDD)! Part 1 of this book gives you both a high-level view of the world of BDD and a first taste of what BDD looks like in the field.

在第 1 章和第 2 章中,您将了解 BDD 的动机和起源,以及它在敏捷和其他软件开发方法中的地位。您将发现 BDD 的广泛范围,了解它如何应用于软件开发的各个层面,从高级需求发现和规范到详细的低级编码。您还将了解不仅正确构建软件,而且构建正确的软件的重要性。

In chapters 1 and 2, you’ll learn about the motivations and origins of BDD and where it sits with regard to Agile and other software development approaches. You’ll discover the broad scope of BDD, learning how it applies at all levels of software development, from high-level requirements discovery and specification to detailed low-level coding. And you’ll learn how important it is not only to build the software right, but also to build the right software.

作为从业者,我们喜欢以现实世界的例子为基础,因此在第 3 章中,您将看到 BDD 在真实项目中的样子,从发现需求和自动化高级验收标准到构建和验证设计和实施,通过生成准确和最新的技术和功能文档。

As practitioners, we like to keep things grounded in real-world examples, so in chapter 3 you’ll see what BDD looks like in a real project, from discovering the requirements and automating the high-level acceptance criteria to building and verifying the design and implementation, through producing accurate and up-to-date technical and functional documentation.

在第 1 部分结束时,您应该很好地掌握 BDD 的动机和整体广泛范围,以及了解它在软件开发过程的不同级别的实践中的情况。

By the end of part 1, you should have a good grasp of the motivations and overall broad scope of BDD, as well as an idea of what it looks like in practice at different levels of the software development process.

1 构建与众不同的软件

1 Building software that makes a difference

本章封面

This chapter covers

  • 行为驱动开发解决的问题
  • The problems that Behavior-Driven Development addresses
  • 行为驱动开发的一般原则和起源
  • General principles and origins of Behavior-Driven Development
  • 行为驱动开发项目中的活动和结果
  • Activities and outcomes seen in a Behavior-Driven Development project
  • 行为驱动开发的优缺点
  • The pros and cons of Behavior-Driven Development

本书是关于如何构建和交付运行良好、易于更改和维护的软件,但更重要的是,它关于如何构建为用户提供真正价值的软件。我们希望构建出色的软件,但我们也需要构建值得构建的软件。

This book is about building and delivering software that works well and is easy to change and maintain, but more importantly, it’s about building software that provides real value to its users. We want to build software well, but we also need to build software that’s worth building.

2012 年,美国空军决定放弃一项已耗资超过 10 亿美元的大型软件项目。远征作战支援系统旨在实现供应链管理现代化和精简,从而节省数十亿美元并满足新的立法要求。但经过七年的开发,该系统仍然“未产生任何重大的军事能力”。1空军估计,还需要11亿美元才能完成原计划的四分之一,而且该解决方案要到 2020 年才能推出,比立法截止日期 2017 年晚了三年。

In 2012, the US Air Force decided to ditch a major software project that had already cost over $1 billion. The Expeditionary Combat Support System was designed to modernize and streamline supply chain management in order to save billions of dollars and meet new legislative requirements. But after seven years of development, the system had still “not yielded any significant military capability.”1 The Air Force estimated that an additional $1.1 billion would be required to deliver just a quarter of the original scope and that the solution could not be rolled out until 2020, three years after the legislative deadline of 2017.

这种现象在软件行业中屡见不鲜。根据多项研究,大约一半的软件项目都未能实现重大目标。Standish Group 2011 年版的年度CHAOS 报告发现,42% 的项目交付延迟、超出预算或未能提供所有要求的功能,2 21% 的项目被彻底取消。Scott Ambler 的年度 IT 项目成功率调查采用了更灵活的成功定义,但仍发现失败率高达 30-50%,具体取决于所用的方法。3意味着数十亿美元的努力被浪费,编写的软件最终不会被使用,或者无法解决其旨在解决的业务问题。

This happens a lot in the software industry. According to a number of studies, around half of all software projects fail to deliver in some significant way. The 2011 edition of the Standish Group’s annual CHAOS Report found that 42% of projects were delivered late, ran over budget, or failed to deliver all of the requested features,2 and 21% of projects were cancelled entirely. Scott Ambler’s annual survey on IT project success rates uses a more flexible definition of success, but still found a 30–50% failure rate, depending on the methodologies used.3 This corresponds to billions of dollars in wasted effort, writing software that ultimately won’t be used or that doesn’t solve the business problem it was intended to solve.

如果不必这样会怎样?如果我们能够以一种让我们发现并集中精力于真正重要的事情的方式来编写软件会怎样?如果我们能够客观地了解哪些功能真正有益于组织并了解最具成本效益的实现方式会怎样?如果我们能够超越用户的要求并构建用户真正需要的东西会怎样?

What if it didn’t have to be this way? What if we could write software in a way that would let us discover and focus our efforts on what really matters? What if we could objectively learn what features will really benefit the organization and learn the most cost-effective way to implement them? What if we could see beyond what the user asks for and build what the user actually needs?

组织正在探索如何做到这一点。许多团队正在成功合作,以构建和交付更有价值、更有效、更可靠的软件。他们正在学习如何更快、更有效地做到这一点。在这本书中,您将看到如何做到这一点。我们将探讨多种方法和技术,这些方法和技术归类在行为驱动开发(BDD) 的总体标题下。

Organizations are discovering how to do just that. Many teams are successfully collaborating to build and deliver more valuable, more effective, and more reliable software. And they’re learning to do this faster and more efficiently. In this book, you’ll see how. We’ll explore a number of methods and techniques, grouped under the general heading of Behavior-Driven Development (BDD).

BDD 是一种协作开发方法,团队通过结构化的对话讨论业务规则和预期行为的示例和反例,以建立对真正有益于用户和整个业务的功能的深刻、共同的理解。他们经常以可执行格式表达这些示例,作为验证软件行为的自动验收测试的基础。BDD 帮助团队将精力集中在识别、理解和构建对业务至关重要的有价值的功能上,并确保这些功能得到良好的设计和实施。

BDD is a collaborative development approach where teams use structured conversations about examples and counterexamples of business rules and expected behavior to build a deep, shared understanding of the features that will really benefit the users and the business as a whole. Very often they express these examples in an executable format that acts as the basis for automated acceptance tests that validate the software’s behavior. BDD helps teams focus their efforts on identifying, understanding, and building valuable features that matter to businesses, and it makes sure that these features are well designed and well implemented.

BDD 实践者通过围绕系统行为的具体示例进行对话,帮助理解功能如何为业务提供价值。它鼓励业务分析师、软件开发人员和测试人员更紧密地协作,使他们能够以更易于测试的方式表达需求,让开发团队和业务利益相关者都能轻松理解。BDD 工具可以帮助将这些需求转化为自动化测试,帮助指导开发人员、验证功能并记录应用程序的功能。

BDD practitioners use conversations around concrete examples of system behavior to help understand how features will provide value to the business. It encourages business analysts, software developers, and testers to collaborate more closely by enabling them to express requirements in a more testable way, in a form that both the development team and business stakeholders can easily understand. BDD tools can help turn these requirements into automated tests that help guide the developer, verify the feature, and document what the application does.

BDD 本身并不是软件开发方法。它不能替代 Scrum、XP、Kanban 或您当前使用的任何方法。正如您所看到的,BDD 融合、构建和增强了许多这些方法的理念。无论您使用哪种方法,BDD 都可以帮助您简化生活。

BDD isn’t a software development methodology in its own right. It’s not a replacement for Scrum, XP, Kanban, or whatever methodology you’re currently using. As you’ll see, BDD incorporates, builds on, and enhances ideas from many of these methodologies. And no matter what methodology you’re using, there are ways that BDD can help make your life easier.

1.1 从 50,000 英尺高度进行 BDD

1.1 BDD from 50,000 feet

所以BDD 能带来什么?以下是(略微过于简化的)观点。假设 Chris 的公司需要为其会计软件添加一个新模块。当 Chris 想要添加新功能时,流程大致如下(见图 1.1):

So what does BDD bring to the table? Here’s a (slightly oversimplified) perspective. Let’s say Chris’s company needs a new module for its accounting software. When Chris wants to add a new feature, the process goes something like this (see figure 1.1):

  1. 克里斯告诉业务分析师他希望该功能如何发挥作用。

  2. Chris tells a business analyst how he would like the feature to work.

  3. 业务分析师将 Chris 的请求转化为开发人员的一组需求,描述软件应该做什么。这些需求以英文书写,并存储在 Microsoft Word 文档中。

  4. The business analyst translates Chris’s requests into a set of requirements for the developers, describing what the software should do. These requirements are written in English and stored in a Microsoft Word document.

  5. 开发人员将需求转化为代码和单元测试(用 Java、C# 或其他编程语言编写),以实现新功能。

  6. The developer translates the requirements into code and unit tests—written in Java, C#, or some other programming language—in order to implement the new feature.

  7. 测试人员将Word文档中的需求转化为测试用例,并用它们来验证新功能是否满足需求。

  8. The tester translates the requirements in the Word document into test cases and uses them to verify that the new feature meets the requirements.

  9. 然后,文档工程师将工作软件和代码翻译回简单的英语技术和功能文档。

  10. Documentation engineers then translate the working software and code back into plain English technical and functional documentation.

图 1.1 传统的开发流程为误解和沟通不畅提供了许多机会。

Figure 1.1 The traditional development process provides many opportunities for misunderstandings and miscommunication.

在此过程中,信息很有可能在翻译过程中丢失、被误解或被忽略。新模块本身可能不能完全满足要求,文档也可能不能反映 Chris 向分析师提出的初始要求。

Along the way there are many opportunities for information to get lost in translation, be misunderstood, or just be ignored. Chances are that the new module itself may not do exactly what was required and that the documentation won’t reflect the initial requirements that Chris gave the analyst.

Chris 的朋友 Sarah 经营着另一家刚刚引入 BDD 的公司。在实践 BDD 的团队中,业务分析师、开发人员和测试人员协作以了解和定义需求(见图 1.2)。他们用一种通用语言表达需求,这有助于团结和集中团队的努力。他们甚至可以将这些需求转化为自动验收测试,既指定软件应如何运行,又证明交付的软件按应有的方式运行。我们可以在图 1.2 中看到这个流程。

Chris’s friend Sarah runs another company that’s just introduced BDD. In a team practicing BDD, the business analysts, developers, and testers collaborate to understand and define the requirements (see figure 1.2). They express the requirements in a common language that helps unite and focus the team’s efforts. They can even turn these requirements into automated acceptance tests that both specify how the software should behave and also demonstrate that the delivered software behaves as it should. We can see this flow in figure 1.2.

  1. 和克里斯一样,莎拉也会与业务分析师贝琳达交谈,以便从高层次了解自己想要什么。但她并不是一个人这样做:她与一名开发人员和一名测试人员一起,直接了解用户真正需要什么。为了减少误解和隐藏假设的风险,他们通过示例讨论该功能应该做什么和不应该做什么。他们试图阐明他们试图解决的业务问题、他们想要实现的业务目标以及哪些功能和能力可能有助于实现这一目标。

  2. Like Chris, Sarah talks to Belinda, the business analyst, to get a high-level vision of what she wants. But she doesn’t do so alone: she is joined by a developer and a tester who get to hear firsthand what the users really need. To reduce the risk of misunderstandings and hidden assumptions, they talk through examples of what the feature should do and what it shouldn’t. They try to articulate the business problem they are trying to solve, the business goal they are aiming for, and what features and capabilities might help achieve this goal.

  3. 在开始开发该功能之前,Belinda 会与负责该功能的开发人员和测试人员聚在一起,讨论该功能。在这些对话中,他们讨论了该功能的关键业务目标和结果,并通过具体示例和反例来更深入地了解需求。通常,对于更重要的功能,Sarah 也会参与此对话。

    经过这次谈话后,团队成员会以结构化、业务可读且接近纯英语的格式编写关键示例和反例。这些示例既可作为功能规范,也可作为自动验收测试的基础。

  4. Before work starts on the feature, Belinda gets together with the developer and tester who will be working on it, and they have a conversation about the feature. In these conversations, they discuss the key business goals and outcomes of the feature and work through concrete examples and counterexamples to get a deeper understanding of the requirement. Oftentimes, for more important features, Sarah will participate in this conversation as well.

    After this conversation, team members write up the key examples and counterexamples in a structured, business-readable format that is quite close to plain English. These examples act both as specifications of the features and as the basis for automated acceptance tests.

  5. 开发人员和测试人员将这些“可执行规范”转变为自动化验收测试;这些自动化测试有助于指导开发过程并确定功能何时完成。

  6. The developers and testers turn these “executable specifications” into automated acceptance tests; these automated tests help guide the development process and determine when a feature is finished.

  7. 当自动验收测试通过时,团队就有确凿的证据证明该功能实现了第 2 阶段所商定的功能。测试人员可以将这些测试的结果用作需要进行的任何手动和探索性测试的起点。

  8. When the automated acceptance tests pass, the team has concrete proof that the feature does what was agreed on in phase 2. The tester might use the results of these tests as the starting point for any manual and exploratory testing that needs to be done.

  9. 自动化测试还可作为产品文档,提供系统工作原理的精确且最新的示例。Sarah 可以查看测试报告,了解已交付的功能以及它们是否按她预期的方式运行。

  10. The automated tests also act as product documentation, providing precise and up-to-date examples of how the system works. Sarah can review the test reports to see what features have been delivered and whether they perform the way she expected.

图 1.2 BDD 使用围绕业务规则和示例的对话,以易于自动化的形式表达,以减少信息丢失和误解。

Figure 1.2 BDD uses conversations around business rules and examples, expressed in a form that can be easily automated, to reduce lost information and misunderstandings.

与 Chris 的情况相比,Sarah 的团队大量使用对话和示例来减少翻译过程中丢失的信息量。第 2 步之后的每个阶段都以结构化但业务可读的风格编写规范开始,这些规范基于 Sarah 提供的具体示例。通过这种方式,在将客户的初始需求转化为代码、报告和文档时,消除了大量的歧义。

Compared to Chris’s scenario, Sarah’s team makes heavy use of conversations and examples to reduce the amount of information lost in translation. Every stage beyond step 2 starts with the specifications written in a structured but business-readable style, which are based on concrete examples provided by Sarah. In this way, a great deal of the ambiguity in translating the client’s initial requirements into code, reports, and documentation is removed.

我们将在本书的其余部分详细讨论所有这些要点。您将学习一些方法,以帮助确保您的代码是高质量、可靠的、经过良好测试的和有良好文档的。您将学习如何编写更有效的单元测试和更有意义的自动验收标准。您还将学习如何确保您提供的功能解决了正确的问题,并为用户和开发人员带来真正的好处。商业。

We’ll discuss all of these points in detail throughout the rest of the book. You’ll learn ways to help ensure that your code is of high quality, solid, well tested, and well documented. You’ll learn how to write more effective unit tests and more meaningful automated acceptance criteria. You’ll also learn how to ensure that the features you deliver solve the right problems and provide real benefit to the users and the business.

1.2 您要解决什么问题?

1.2 What problems are you trying to solve?

软件项目失败的原因有很多,但最重要的原因可以分为两大类:

Software projects fail for many reasons, but the most significant causes fall into two broad categories:

  • 软件构建不正确

  • Not building the software right

  • 没有构建正确的软件

  • Not building the right software

图 1.3 以图表的形式说明了这一点。纵轴表示您正在构建什么,横轴表示您如何构建它。如果您在“如何”轴上表现不佳,没有编写精心制作和设计的软件,那么您最终会得到一个有缺陷、不可靠的产品,很难更改和维护。如果您在“什么”轴上表现不佳,无法理解企业真正需要什么功能,那么您最终会得到一个没人需要的产品。

Figure 1.3 illustrates this in the form of a graph. The vertical axis represents what you’re building, and the horizontal axis represents how you build it. If you perform poorly on the how axis, not writing well-crafted and well-designed software, you’ll end up with a buggy, unreliable product that’s hard to change and maintain. If you don’t do well on the what axis, failing to understand what features the business really needs, you’ll end up with a product that nobody needs.

图 1.3 成功的项目必须既构建良好的功能,又构建正确的功能。

Figure 1.3 Successful projects must both build features well and build the right features.

1.2.1 构建正确的软件

1.2.1 Building the software right

许多软件质量问题会导致项目受损或失败。尽管内部软件质量对于非技术利益相关者来说大多是不可见的,但劣质软件的后果却是显而易见的。根据我们的经验,设计不佳、编写不当或缺乏编写良好的自动化测试的应用程序往往存在缺陷、难以维护、难以更改且难以扩展。

Many projects suffer or fail because of software quality problems. Although internal software quality is mostly invisible to nontechnical stakeholders, the consequences of poor-quality software can be painfully visible. In our experience, applications that are poorly designed, badly written, or lack well-written, automated tests tend to be buggy, hard to maintain, hard to change, and hard to scale.

我们已经看到太多应用程序,简单的变更请求和新功能需要很长时间才能交付。开发人员花费越来越多的时间修复错误,而不是开发新功能,这使得快速交付新功能变得更加困难。新开发人员需要更长的时间才能上手并提高工作效率,仅仅是因为代码很难理解。在不破坏现有代码的情况下添加新功能也变得越来越困难。现有的技术文档(如果有的话)不可避免地已经过时,团队发现自己无法快速交付新功能,因为每次发布都需要长时间的手动测试和错误修复。

We’ve seen too many applications where simple change requests and new features take too long to deliver. Developers spend more and more time fixing bugs rather than working on new features, which makes it harder to deliver new features quickly. It takes longer for new developers to get up to speed and become productive, simply because the code is hard to understand. It also becomes harder and harder to add new features without breaking existing code. The existing technical documentation (if there is any) is inevitably out of date, and teams find themselves incapable of delivering new features quickly because each release requires a lengthy period of manual testing and bug fixes.

采用高质量技术实践的组织则有不同的故事。我们已经看到许多采用测试驱动开发、干净编码、动态文档和持续集成等实践的团队定期报告低至接近零的缺陷率,以及随着新需求的出现和新功能的请求,代码更容易适应和扩展。这些团队还可以以更一致的速度添加功能,因为自动化测试可确保现有功能不会在不知不觉中被破坏。他们比其他团队更快、更准确地实现这些功能,因为他们不必在进行更改时费力地进行长时间的错误修复和不可预测的副作用。而且,最终的应用程序维护起来更容易、更便宜。

Organizations that embrace high-quality technical practices have a different story to tell. We’ve seen many teams that adopt practices such as Test-Driven Development, Clean Coding, Living Documentation, and Continuous Integration regularly reporting low to near-zero defect rates, as well as code that’s much easier to adapt and extend as new requirements emerge and new features are requested. These teams can also add features at a more consistent pace, because the automated tests ensure that existing features won’t be broken unknowingly. They implement the features faster and more precisely than other teams because they don’t have to struggle with long bug-fixing sessions and unpredictable side effects when they make changes. And the resulting application is easier and cheaper to maintain.

请注意,没有神奇的公式可以构建高质量、易于维护的软件。软件开发是一个复杂的领域,人为因素比比皆是,测试驱动开发、干净编码和自动测试等技术并不能自动保证良好的结果。但研究确实表明,与更传统的方法相比,精益和敏捷实践与项目成功率4之间存在很强的相关性。其他研究发现测试驱动开发实践与减少错误数量5和提高代码质量6之间存在相关性。虽然不实践测试驱动开发和干净编码等技术也完全可以编写高质量的代码,但重视良好开发实践的团队似乎确实能够更频繁地成功交付高质量的代码。

Note that there is no magic formula for building high-quality, easily maintainable software. Software development is a complex field, human factors abound, and techniques such as Test-Driven Development, Clean Coding, and Automated Testing don’t automatically guarantee good results. But studies do suggest a strong correlation between lean and Agile practices and project success rates4 when compared to more traditional approaches. Other studies have found a correlation between Test-Driven Development practices, reduced bug counts,5 and improved code quality.6 Although it’s certainly possible to write high-quality code without practicing techniques such as Test-Driven Development and Clean Coding, teams that value good development practices do seem to succeed in delivering high-quality code more often.

但开发高质量的软件本身并不足以保证项目的成功。软件还必须让用户和企业受益利益相关者。

But building high-quality software isn’t in itself enough to guarantee a successful project. The software must also benefit its users and business stakeholders.

1.2.2 构建正确的软件

1.2.2 Building the right software

软件软件开发从来都不是凭空而来的。软件项目是更广泛的商业战略的一部分,如果它们想对组织有益,就需要与业务目标保持一致。归根结底,您提供的软件解决方案需要帮助用户更有效地实现他们的目标。任何无法实现这一目标的努力都是浪费。

Software is never developed in a vacuum. Software projects are part of a broader business strategy, and they need to be aligned with business goals if they’re to be beneficial to the organization. At the end of the day, the software solution you deliver needs to help users achieve their goals more effectively. Any effort that doesn’t contribute to this end is wasted.

实际上,浪费现象非常普遍。在许多项目中,时间和金钱都花在了开发从未使用过或只为业务提供边际价值的功能上。根据 Standish Group 的 CHAOS 研究,平均有 45% 的功能从未使用过。即使是看似可预测的项目,例如将软件从大型机系统迁移到更现代的平台,也有一些功能需要更新或不再需要。当您不完全了解客户想要实现的目标时,很容易交付功能完美、编写精良但对最终用户用处不大的功能。

In practice, there’s often a lot of waste. In many projects, time and money are spent building features that are never used or that provide only marginal value to the business. According to the Standish Group’s CHAOS studies,7 on average some 45% of the features delivered into production are never used. Even apparently predictable projects, such as migrating software from a mainframe system onto a more modern platform, have their share of features that need updating or that are no longer necessary. When you don’t fully understand the goals that your client is trying to achieve, it’s very easy to deliver perfectly functional, well-written features that are of little use to the end user.

另一方面,许多软件项目最终几乎没有产生任何实际商业价值。它们不仅提供对业务用处不大的功能,甚至连使项目可行的最低限度的功能都没有提供。

On the other hand, many software projects end up delivering little or no real business value. Not only do they deliver features that are of little use to the business, but they fail to even deliver the minimum capabilities that would make the projects viable.

构建不当和构建不正确的事物的后果

The consequences of not building it right and not building the right thing

需求理解不充分和代码实现不佳的影响不仅仅是一个理论概念或“有则更好”;相反,它往往是痛苦的具体表现。2007 年 12 月,昆士兰卫生部开始为其 85,000 名员工开发一套新的工资系统。该项目的初始预算约为 600 万美元,交付日期为 2008 年 8 月。

The affect of poorly understood requirements and poor code realization isn’t just a theoretical concept or a “nice to have;” on the contrary, it’s often painfully concrete. In December 2007, the Queensland Health Department kicked off work on a new payroll system for its 85,000 employees. The initial budget for the project was around $6 million, with a delivery date of August 2008.

该解决方案于 2010 年推出,比原计划晚了 18 个月,结果却是个灾难。数以万计的公务员被少付工资、多付工资,甚至被拖欠工资。自上线以来,超过 1,000 名薪资工作人员每两周需要执行约 200,000 个手动流程,以确保员工工资得到支付。

When the solution was rolled out in 2010, some 18 months late, it was a disaster. Tens of thousands of public servants were underpaid, overpaid, or not paid at all. Since the go-live date, over 1,000 payroll staff have been required to carry out some 200,000 manual processes each fortnight to ensure that staff salaries are paid.

2012 年,一项独立审查发现,该项目自投入生产以来已耗费该州 4.16 亿美元,修复成本将达到 8.37 亿美元。这笔巨款中,仅修复当前软件问题就花费了 2.2 亿美元,这些问题导致该系统无法发挥其核心功能,即每月向昆士兰卫生部门员工支付应得工资。

In 2012, an independent review found that the project had cost the state over $416 million since going into production and would cost an additional $837 million to fix. This colossal sum included $220 million just to fix the immediate software problems that were preventing the system from delivering its core capability of paying Queensland Health staff what they were owed each month.

一个常被忽视的事实使得开发正确的软件变得更加棘手:在项目早期,你通常不知道什么是正确的功能。8

Building the right software is made even trickier by one commonly overlooked fact: early on in a project, you usually don’t know what the right features are.8

正如我们将在本书的其余部分看到的那样,BDD 是解决这两个问题的一种非常有效的方法。它的主要方法之一是解决软件项目中风险和超支的主要原因之一:团队对他们应该做什么没有足够的认识建筑。

As we will see in the rest of this book, BDD is a very effective way to address both of these problems. And one of the main ways it does so is by tackling one of the principle causes of risk and overrun in software projects: the team not having enough clarity on what they are supposed to be building.

1.2.3 知识约束:应对不确定性

1.2.3 The knowledge constraint: Dealing with uncertainty

软件开发中,总会有一些你不知道的事情。需求变化是每个软件项目的正常组成部分。对当前问题以及如何最好地解决问题的知识和理解会在整个项目中逐渐增加。

One fact of life in software development is that there will be things you don’t know. Changing requirements are a normal part of every software project. Knowledge and understanding about the problem at hand and about how best to solve it increases progressively throughout the project.

在软件开发中,每个项目都是不同的。总是有新的业务需求需要满足,新的技术问题需要解决,以及新的机遇需要抓住。随着项目的进展,市场条件、业务策略、技术限制或您对需求的理解都会发生变化,您需要改变策略并调整方向。每个项目都是一次探索之旅,真正的限制不是时间、预算,甚至不是程序员的工作时间,而是您对需要构建什么以及如何构建它缺乏了解。当现实没有按照计划进行时,您需要适应现实,而不是试图强迫现实适应您的计划。“当地形与地图不一致时,请相信地形”(瑞士军队谚语)。

In software development, each project is different. There are always new business requirements to cater to, new technological problems to solve, and new opportunities to seize. As a project progresses, market conditions, business strategies, technological constraints, or simply your understanding of the requirements will evolve, and you’ll need to change your tack and adjust your course. Each project is a journey of discovery, where the real constraint isn’t time, the budget, or even programmer hours, but your lack of knowledge about what you need to build and how you should build it. When reality doesn’t go according to plan, you need to adapt to reality, rather than trying to force reality to fit into your plan. “When the terrain disagrees with the map, trust the terrain” (Swiss Army proverb).

用户和利益相关者通常会知道他们想要实现的高层目标,如果你花时间询问,他们就会被诱导透露这些目标。他们会告诉你,他们需要一个在线票务系统或一个能满足 85,000 名不同员工需求的工资单解决方案。你可以在项目早期了解可能需要构建的应用程序的范围。

Users and stakeholders will usually know what high-level goals they want to achieve and can be coaxed into revealing these goals if you take the time to ask. They’ll be able to tell you that they need an online ticketing system or a payroll solution that caters to 85,000 different employees. And you can get a feel for the scope of the application you might need to build early on in the project.

但细节则完全是另一回事。尽管用户会迅速要求针对其问题提供具体的技术解决方案,但他们通常并不清楚哪种解决方案最适合他们,甚至不清楚存在哪些解决方案。随着项目的进展,您的团队对提供这些功能的最佳方式以及实现基本业务目标的最佳功能集的集体理解将不断增长。

But the details are another matter entirely. Although users are quick to ask for specific technical solutions to their problems, they’re not usually the best placed to know what solution would serve them best, or even what solutions exist. Your team’s collective understanding of the best way to deliver these capabilities, as well as the optimal feature set for achieving the underlying business goals, will grow as the project progresses.

如图 1.4 所示,更具规范性、基于计划的需求分析技术假设您可以在项目早期阶段非常快速地了解项目需求的几乎所有信息以及最佳解决方案设计。到分析阶段结束时,规范已签署并锁定,剩下要做的就是编码。

As illustrated in figure 1.4, the more prescriptive, plan-based requirements-analysis techniques suppose that you can learn almost all there is to know about a project’s requirements, as well as the optimal solution design, very quickly in the early phases of the project. By the end of the analysis phase, the specifications are signed-off on and locked down, and all that remains to do is code.

图 1.4 项目开始时,有许多未知数。随着项目的进展,这些未知数会逐渐减少,但并不是以线性或可预测的方式减少。

Figure 1.4 At the start of a project, there are many unknowns. You reduce these unknowns as the project progresses, but not in a linear or very predictable way.

当然,现实并不总是如此。在项目开始时,开发团队通常对业务领域和用户需要实现的目标只有肤浅的了解。事实上,软件工程团队的工作不是知道如何构建解决方案;而是知道如何发现构建解决方案的最佳方法。

Of course, reality doesn’t always work this way. At the start of the project, a development team will often have only a superficial understanding of the business domain and the goals the users need to achieve. In fact, the job of a software engineering team isn’t to know how to build a solution; it’s to know how to discover the best way to build the solution.

随着项目的进行,团队的集体理解自然会增强。随着时间的推移,你会变得不那么无知。在项目结束时,一个好的团队将对用户的需求建立起深刻而深入的了解,并能够主动提出更适合特定用户群的功能和实现。但这条学习路径既不是线性的,也不是可预测的。很难知道你不知道什么,所以很难预测随着项目的进展你会学到什么。

The team’s collective understanding will naturally increase over the duration of the project. You become less ignorant over time. Toward the end of the project, a good team will have built up a deep, intimate knowledge of the user’s needs and will be able to proactively propose features and implementations that will be better suited to the particular user base. But this learning path is neither linear nor predictable. It’s hard to know what you don’t know, so it’s hard to predict what you’ll learn as the project progresses.

对于大多数现代软件开发项目来说,管理范围的主要挑战不是通过尽早定义和锁定需求来消除不确定性。主要挑战是以一种能够帮助您逐步发现和提供与项目背后的基本业务目标相匹配的有效解决方案的方式来管理这种不确定性。正如您所看到的,BDD 的一个重要好处是它提供了可以帮助您管理这种不确定性并降低由此带来的风险的技术它。

For the majority of modern software development projects, the main challenge in managing scope isn’t to eliminate uncertainty by defining and locking down requirements as early as possible. The main challenge is to manage this uncertainty in a way that will help you progressively discover and deliver an effective solution that matches up with the underlying business goals behind a project. As you’ll see, one important benefit of BDD is that it provides techniques that can help you manage this uncertainty and reduce the risk that comes with it.

1.3 BDD 适合您的项目吗?

1.3 Is BDD right for your projects?

身体缺陷诊断可以很好地与 Scrum 等其他敏捷方法配合使用,但也可以与 Kanban 等精益方法一起使用。它不是一种独立的方法,而是一种实践集合,可以帮助团队更快、更有效地发现和理解业务需求,并自动获得反馈,判断他们构建的功能是否确实满足了这些需求。例如,使用 BDD 的 Scrum 团队的工作方式与普通 Scrum 团队大致相同,但他们将在积压工作细化会议期间应用 BDD 实践,以更清楚地了解他们需要构建的功能。他们还将更加关注冲刺中的自动化,并尝试为冲刺期间交付的功能的验收标准编写自动化测试。实践 BDD 的 Scrum 团队还希望将“通过自动验收测试”添加到用户故事的定义中。

BDD works well with other Agile methodologies such as Scrum, but it can also be used with lean approaches such as Kanban. It is not a separate methodology, but more a collection of practices that helps teams discover and understand business needs more quickly and more effectively and get automated feedback on whether these needs have indeed been met by the features they build. For example, a Scrum team using BDD will work in much the same way as an ordinary Scrum team, but they will apply BDD practices during their backlog refinement sessions to get more clarity on the features they need to build. They will also pay more attention to in-sprint automation and try to write automated tests for the acceptance criteria for the features they deliver during the sprint. A Scrum team practicing BDD will also want to add “passing automated acceptance tests” to the definition of their user stories.

BDD 适用于任何类型的需求发现,无论是在绿地项目中还是在已经在进行的项目。本书中的示例主要侧重于构建新功能和新应用程序(第 14 章和第 15 章除外,我们专门研究如何使用遗留应用程序)。这是有意为之,以使领域更容易理解,示例更具吸引力。然而,BDD 对现有应用程序和复杂领域也非常有效。两位作者都花了大量时间在大型金融机构从事复杂项目。事实上,您将在本书中学习的技术适用于任何领域,只要需求并不简单,需要揭示假设,并且每个故事的表面下都存在复杂性和不确定性,这几乎涵盖了我们曾经做过的每个项目在。

BDD works well for any kind of requirements discovery, both in green fields projects and in ones that are already underway. The examples in this book focus mostly on building new features and new applications (with the exception of chapters 14 and 15, where we look specifically at working with legacy applications). This is by design, to make the domains easier to understand and the examples more engaging. However, BDD is also very effective for existing applications and complex domains. Both authors spend much of their time working in large financial organizations on complex projects. In fact, the techniques you will learn in this book apply to any domain where the requirements are not trivial, where assumptions need to be uncovered, and where complexity and uncertainty lie underneath the surface of each story, which covers almost every project we have ever worked on.

1.4 您将在本书中学到什么

1.4 What you will learn in this book

本书不仅让您了解 BDD 的理论基础,还让您了解将 BDD 引入您自己的组织需要做什么的实践知识。您将学到以下内容:

This book gives you both an understanding of the theoretical foundations of BDD, and hands-on knowledge about what you need to do to introduce BDD into your own organization. You will learn the following:

  • 如何使用协作技术和敏捷需求来明确真实用户需求,以发现示例映射和特征映射等技术

  • How to get clarity on real user requirements using collaborative techniques and Agile requirements to discover techniques such as Example Mapping and Feature Mapping

  • 如何以可执行格式记录这些要求(可执行规范)可以充当自动化测试并集成到您的构建过程中

  • How to record these requirements in an executable format (executable specifications) that can act as automated tests and be integrated into your build process

  • 如何使用 Java 或 JavaScript 以及 Cucumber 等工具自动化这些可执行规范

  • How to automate these executable specifications using Java or JavaScript, and using tools such as Cucumber

  • 如何使用这些可执行规范来生成动态文档,以验证和记录您交付的功能

  • How to use these executable specifications to produce living documentation that both verifies and documents the features you deliver

概括

Summary

  • 成功项目的开发人员需要构建可靠、无错误的软件(正确构建软件)并构建可为企业带来真正价值的功能(构建正确的软件)。

  • The developers of successful projects need to build software that’s reliable and bug free (build the software right) and to build features that deliver real value to the business (build the right software).

  • BDD 是一种协作开发方法,团队通过结构化对话讨论业务规则和预期行为的示例和反例,以建立对哪些功能将使用户受益的深刻共识。他们通常会以可执行格式表达这些示例,作为验证软件行为的自动验收测试的基础。

  • BDD is a collaborative development approach where teams use structured conversations about examples and counterexamples of business rules and expected behavior to build a deep, shared understanding of what features will benefit users. Very often they express these examples in an executable format that acts as the basis for automated acceptance tests that validate the software’s behavior.

  • BDD 从业者通过有关具体示例的对话来建立对哪些功能将为组织带来真正价值的共同理解。

  • BDD practitioners use conversations about concrete examples to build a common understanding of what features will deliver real value to the organization.

  • 这些示例构成了开发人员用来确定功能何时完成的验收标准的基础。

  • These examples form the basis of the acceptance criteria that developers use to determine when a feature is done.

在下一章中,我们将回顾 BDD 的起源,并进一步了解 BDD 流程的关键步骤。细节。

In the next chapter, we’ll look at the origins of BDD and learn about the key steps of BDD process in more detail.


1  Chris Kanaracus,“空军在花费 10 亿美元后放弃大型 ERP 项目”,CIO,2012 年 11 月 14 日,https://www.computerworld.com/article/2493041/air-force-scraps-massive-erp-project-after-racking-up--1b-in-costs.xhtml

1  Chris Kanaracus, “Air Force scraps massive ERP project after racking Up $1 billion in costs,” CIO, November 14, 2012, https://www.computerworld.com/article/2493041/air-force-scraps-massive-erp-project-after-racking-up--1b-in-costs.xhtml.

2  这些数字是否更多地反映了我们构建和交付软件的能力,还是更多地反映了我们规划和估算的能力,这是敏捷开发社区中争论的话题——请参阅 Jim Highsmith 的书《敏捷项目管理:创造创新产品》,第二版(Addison-Wesley Professional,2009 年)。

2  Whether these figures reflect more on our ability to build and deliver software or on our ability to plan and estimate is a subject of some debate in the Agile development community—see Jim Highsmith’s book Agile Project Management: Creating Innovative Products, second edition (Addison-Wesley Professional, 2009).

3  Scott Ambler,“探索信息技术实践现状的调查”,http://www.ambysoft.com/surveys/

3  Scott Ambler, “Surveys Exploring the Current State of Information Technology Practices,” http://www.ambysoft.com/surveys/.

4  例如,请参阅 Scott Wambler 的“2018 年 IT 项目成功率调查结果”,http://www.ambysoft.com/surveys/success2018.xhtml

4  See, for example, Scott Wambler, “2018 IT Project Success Rates Survey Results,” http://www.ambysoft.com/ surveys/success2018.xhtml.

5  例如,请参阅 Nachiappan Nagappan、E. Michael Maximilien、Thirumalesh Bhat 和 Laurie Williams 撰写的“通过测试驱动开发实现质量改进:四个工业团队的成果和经验”,https://www.microsoft.com/en-us/research/wp-content/uploads/2009/10/Realizing-Quality-Improvement-Through-Test-Driven-Development-Results-and-Experiences-of-Four-Industrial-Teams-nagappan_tdd.pdf

5  See, for example, Nachiappan Nagappan, E. Michael Maximilien, Thirumalesh Bhat, and Laurie Williams, “Realizing quality improvement through test driven development: results and experiences of four industrial teams,” https://www.microsoft.com/en-us/research/wp-content/uploads/2009/10/Realizing-Quality-Improvement-Through-Test-Driven-Development-Results-and-Experiences-of-Four-Industrial-Teams-nagappan_tdd.pdf.

6  Rod Hilton,“通过将面向对象质量指标应用于开源项目来定量评估测试驱动开发”(博士论文,里吉斯大学,2009 年),http://www.rodhilton.com/files/tdd_thesis.pdf

6  Rod Hilton, “Quantitatively Evaluating Test-Driven Development by Applying Object-Oriented Quality Metrics to Open Source Projects” (PhD thesis, Regis University, 2009), http://www.rodhilton.com/files/tdd_thesis.pdf.

7  Standish Group 的2002 年 CHAOS 报告指出该数字为 45%,而我看到最近的内部研究表明该数字约为 50%。

7  The Standish Group’s CHAOS Report 2002 reported a value of 45%, and I’ve seen more recent internal studies where the figure is around 50%.

8  请参阅毕马威,《昆士兰州卫生工资系统审查》,2012 年,http://delimiter.com.au/wp-content/uploads/2012/06/KPMG_audit.pdf

8  See KPMG, “Review of the Queensland Health Payroll System,” 2012, http://delimiter.com.au/wp-content/uploads/2012/06/KPMG_audit.pdf.

2 引入行为驱动开发

2 Introducing Behavior-Driven Development

本章封面

This chapter covers

  • BDD 的起源
  • The origins of BDD
  • BDD 项目中看到的活动和结果
  • Activities and outcomes seen in a BDD project
  • BDD 的优缺点
  • The pros and cons of BDD

在本章中,我们将更详细地介绍 BDD。BDD 是一套软件工程实践,旨在帮助团队更快地构建和交付更有价值、更高质量的软件。它借鉴了敏捷和精益实践,特别是测试驱动开发 (TDD) 和领域驱动设计 (DDD))。但最重要的是,BDD 提供了一种通用语言,这种语言基于用英语(或利益相关者的母语)表达的简单、结构化的句子,有助于项目团队成员和业务利益相关者之间的沟通。为了更好地理解推动 BDD 实践的动机和理念,了解 BDD 的来源很有用。

In this chapter we will dive into what BDD looks like in a little more detail. BDD is a set of software engineering practices designed to help teams build and deliver more valuable, higher-quality software faster. It draws on Agile and lean practices, including in particular, Test-Driven Development (TDD) and Domain-Driven Design (DDD). But most importantly, BDD provides a common language based on simple, structured sentences expressed in English (or in the native language of the stakeholders) that facilitate communication between project team members and business stakeholders. To better understand the motivations and philosophy that drive BDD practices, it’s useful to understand where BDD comes from.

2.1 BDD 最初是为了让 TDD 教学更容易而设计的

2.1 BDD was originally designed to make teaching TDD easier

身体缺陷诊断最初由 Daniel Terhorst-North 1在 21 世纪初期至中期发明,作为一种更简便的教授和实践 TDD 的方法,而 TDD 是由 Kent Beck 在 Agile 早期发明的。2 TDD是一种非常有效的技术,它使用单元测试来指定、设计和验证应用程序代码。

BDD was originally invented by Daniel Terhorst-North1 in the early to mid-2000s as an easier way to teach and practice TDD, which was invented by Kent Beck in the early days of Agile.2 TDD is a remarkably effective technique that uses unit tests to specify, design, and verify application code.

单元测试和验收测试

Unit tests and acceptance tests

在本章中我们将大量讨论单元测试和验收测试,因此有必要澄清这些术语的含义。

We will be talking a lot about unit tests and acceptance tests in this chapter, so it is worthwhile clarifying what we mean by these terms.

单元测试是描述和验证系统各个组件行为的小型测试。单元测试侧重于系统的内部工作;在现代编程语言中,组件可能是方法或函数。

Unit tests are small tests that describe and verify the behavior of individual components of a system. Unit tests focus on the internal workings of the system; in modern programming languages, a component might be a method or a function.

另一方面,当我们谈论验收测试时,我们指的是面向业务的测试,最终用户或业务赞助商可以使用这些测试来检查功能是否按预期运行。我们还使用可执行规范这一术语来描述可以自动化并以业务可读格式编写的验收测试。

When we talk about acceptance tests, on the other hand, we refer to business-facing tests, tests that end users or business sponsors can use to check that a feature works as intended. We also use the term executable specifications for acceptance tests that can be automated and that are written in a business-readable format.

可执行规范使用业务领域的术语和概念编写。它们旨在让业务人员和最终用户轻松理解,并由整个团队协作定义。另一方面,单元测试是由开发人员为开发人员编写的。编写良好的单元测试描述和记录低级组件行为,就像可执行规范记录和描述用户如何与系统交互一样。

Executable specifications are written using terms and concepts from the business domain. They are designed to be easily understood by business folk and end users and are defined collaboratively by the whole team. Unit tests, on the other hand, are written by developers, for developers. Well-written unit tests describe and document low-level component behavior, much like executable specifications document and describe how users interact with the system.

当 TDD 从业者需要实现某个功能时,他们首先会编写一个失败测试来描述或指定该功能。接下来,他们编写刚好足以通过测试的代码。最后,他们重构代码以确保代码易于维护(见图 2.1)。这种简单但强大的技术鼓励开发人员编写更简洁、设计更好、更易于维护的代码3,从而大幅降低缺陷数量。4

When TDD practitioners need to implement a feature, they first write a failing test that describes, or specifies, that feature. Next, they write just enough code to make the test pass. Finally, they refactor the code to help ensure that it will be easy to maintain (see figure 2.1). This simple but powerful technique encourages developers to write cleaner, better-designed, easier-to-maintain code3 and results in substantially lower defect counts.4

图 2.1 TDD 依赖于一个简单的三相循环。

Figure 2.1 TDD relies on a simple, three-phase cycle.

尽管 TDD 具有诸多优势,但许多团队仍然难以有效采用和使用。开发人员经常不知道从哪里开始或下一步应该编写哪些测试。有时,TDD 会导致开发人员过于注重细节,而忽略了他们应该实现的业务目标的全局。一些团队还发现,随着项目规模的扩大,大量的单元测试可能变得难以维护。

Despite its advantages, many teams still have difficulty adopting and using TDD effectively. Developers often have trouble knowing where to start or what tests they should write next. Sometimes TDD can lead developers to become too detail focused, losing the broader picture of the business goals they’re supposed to implement. Some teams also find that the large numbers of unit tests can become hard to maintain as the project grows in size.

事实上,许多传统的单元测试,无论是否使用 TDD 编写,都与代码的特定实现紧密相关。它们专注于所测试的方法或功能,而不是代码在业务方面应该做什么。

In fact, many traditional unit tests, written with or without TDD, are tightly coupled to a particular implementation of the code. They focus on the method or function they’re testing, rather than on what the code should do in business terms.

例如,假设 Paul 是一名 Java 开发人员,在一家大型银行开发一款新的金融交易应用程序。他被要求实现一项新功能,将资金从一个账户转移到另一个账户。他创建了一个Account使用transfer()方法,一种deposit()方法等等。相应的单元测试重点测试这些方法:

For example, suppose Paul is a Java developer working on a new financial trading application in a large bank. He has been asked to implement a new feature to transfer money from one account to another. He creates an Account class with a transfer() method, a deposit() method, and so on. The corresponding unit tests are focused on testing these methods:

公共类 BankAccountTest {
    @测试
    公共无效测试传输(){...}
    @测试
    公共无效测试存款(){...}
}
public class BankAccountTest {
    @Test
    public void testTransfer() {...}
    @Test
    public void testDeposit() {...}
}

这样的测试总比没有好,但它们会限制你的选择。例如,它们没有描述你期望transfer()deposit()函数做什么,这使得它们更难理解,如果它们出错也更难修复。它们与它们测试的方法紧密耦合,这意味着如果你重构实现,你也需要重命名你的测试。而且因为它们没有说明它们实际测试的内容,所以很难知道在完成之前你需要编写哪些其他测试(如果有的话)。

Tests like this are better than nothing, but they can limit your options. For example, they don’t describe what you expect the transfer() and deposit() functions to do, which makes them harder to understand and to fix if they break. They’re tightly coupled to the method they test, which means that if you refactor the implementation, you need to rename your test as well. And because they don’t say much about what they’re actually testing, it’s hard to know what other tests (if any) you need to write before you’re done.

North 观察到,一些简单的做法,例如将单元测试命名为完整的句子并使用“应该”一词,可以帮助开发人员编写更有意义的测试,从而帮助他们更高效地编写更高质量的代码。当您考虑类应该做什么而不是测试什么方法或功能时,您可以更轻松地将精力集中在底层业务需求上。

North observed that a few simple practices, such as naming unit tests as full sentences and using the word “should,” can help developers write more meaningful tests, which in turn helps them write higher-quality code more efficiently. When you think in terms of what the class should do, instead of what method or function is being tested, it’s easier to keep your efforts focused on the underlying business requirements.

例如,Paul 可以编写更具描述性的测试,如下所示:

For example, Paul could write more descriptive tests along the following lines:

公共类 WhenTransferringInternationalFunds {
    @测试
    公共无效应该将资金转移到本地账户(...)
    @测试
    公共无效应该将资金转移到不同的银行(){...}
    ...
    @测试
    公共无效应该将费用作为单独交易扣除(){...}
    ...
}
public class WhenTransferringInternationalFunds {
    @Test
    public void should_transfer_funds_to_a_local_account() {...}
    @Test
    public void should_transfer_funds_to_a_different_bank() {...}
    ...
    @Test
    public void should_deduct_fees_as_a_separate_transaction() {...}
    ...
}

以这种方式编写的测试读起来更像是规范而不是单元测试。它们专注于应用程序的行为,使用测试只是表达和验证该行为的一种手段。Terhorst-North 还指出,以这种方式编写的测试更容易维护,因为它们的意图非常明确。这种方法的影响是如此显著,以至于他不再将他所做的事情称为 TDD,而是行为驱动开发。如今,BDD 和 TDD 截然不同,尽管它们相关,实践。

Tests that are written this way read more like specifications than unit tests. They focus on the behavior of the application, using tests simply as a means to express and verify that behavior. Terhorst-North also noted that tests written this way are much easier to maintain because their intent is so clear. The affect of this approach was so significant that he no longer referred to what he was doing as TDD, but as Behavior-Driven Development. Nowadays, BDD and TDD are quite distinct, though related, practices.

2.2 BDD 也适用于需求分析

2.2 BDD also works well for requirements analysis

描述系统行为正是业务分析师每天要做的事情。Terhorst-North 与业务分析师同事 Chris Matts 合作,开始将他所学的知识应用到需求分析领域。大约在这个时候,Eric Evans 提出了 DDD 5的概念,它提倡使用业务人员可以理解的通用语言来描述和建模系统。Terhorst-North 和 Matts 的愿景是创建一种通用语言,业务分析师可以使用它来明确定义需求,并且可以轻松转换为自动验收测试。为了实现这一愿景,他们开始以松散结构的示例(称为“场景”)的形式表达用户故事的验收标准,如下所示:

Describing a system’s behavior turns out to be what business analysts do every day. Working with business analyst colleague Chris Matts, Terhorst-North set out to apply what he had learned to the requirements-analysis space. Around this time, Eric Evans introduced the idea of DDD,5 which promotes the use of a ubiquitous language that businesspeople can understand to describe and model a system. Terhorst-North and Matts’s vision was to create a ubiquitous language that business analysts could use to define requirements unambiguously and that could also be easily transformed into automated acceptance tests. To implement this vision, they started expressing the acceptance criteria for user stories in the form of loosely structured examples, known as “scenarios,” like this one:

假设客户有活期账户
当客户将资金从该账户转入海外账户时
然后资金应该存入海外账户
交易费用应从活期账户中扣除
Given a customer has a current account
When the customer transfers funds from this account to an overseas account
Then the funds should be deposited in the overseas account
And the transaction fee should be deducted from the current account

企业主可以轻松理解这样编写的场景。它为每个故事提供了清晰而客观的目标,包括需要开发什么以及需要测试什么。

A business owner can easily understand a scenario written like this. It gives clear and objective goals for each story in terms of what needs to be developed and what needs to be tested.

这种符号最终演变成一种常用的形式,通常称为Gherkin。使用适当的工具,以这种形式编写的场景可以转化为自动化验收标准,可以在需要时自动执行。Terhorst-North 在 2000 年代中期编写了第一个专用的 BDD 测试自动化库 JBehave,从那时起,许多其他针对不同语言的库相继出现,无论是在单元测试还是验收测试水平。

This notation eventually evolved into a commonly used form often referred to as Gherkin. With appropriate tools, scenarios written in this form can be turned into automated acceptance criteria that can be executed automatically whenever required. Terhorst-North wrote the first dedicated BDD test automation library, JBehave, in the mid-2000s, and since then many others have emerged for different languages, both at the unit-testing and acceptance-testing levels.

BDD 的其他名称

BDD by any other name

许多围绕 BDD 的想法并不新鲜,并且已经以许多不同的名称实践了很多年。这些实践中使用的一些更常见的术语包括验收测试驱动开发通过实例进行说明。为了避免混淆,让我们澄清一些与 BDD 相关的术语。

Many of the ideas around BDD are not new and have been practiced for many years under a number of different names. Some of the more common terms used for these practices include Acceptance Test–Driven Development and Specification by Example. To avoid confusion, let’s clarify a few of these terms in relation to BDD.

所有这些实践都属于同一类:如今一些实践者称之为示例引导开发的方法.a具体例子可以极大地帮助理解用户真正需要什么,并且在可能的情况下,在开始某个功能之前以测试的形式自动执行这些示例。

All of these practices belong to the same family: an approach that some practitioners nowadays refer to as Example-Guided Development.a Concrete examples can greatly aid understanding what a user really needs, and, where possible, automating these examples in the form of tests, before work starts on a feature.

验收测试驱动开发或 ATDD,是一种用户与开发人员协作在功能构建之前编写自动验收标准的技术。自 20 世纪 90 年代末以来,该技术以各种形式存在。在早期,这通常被称为故事测试驱动开发。Kent Beck 和 Martin Fowler 在 2000 年提出了这一概念,尽管他们发现在项目开始时以传统单元测试的形式实施验收标准很困难。

Acceptance Test–Driven Development, or ATDD, is a technique where users collaborate with developers to write automated acceptance criteria for features before they are built. The technique has existed in various forms since at least the late 1990s. In the early days, this was often referred to as Story Test-Driven Development. Kent Beck and Martin Fowler mentioned the concept in 2000,b though they observed that it was difficult to implement acceptance criteria in the form of conventional unit tests at the start of a project.

但验收测试不必使用单元测试工具来编写。至少从 21 世纪初开始,创新团队就一直在要求用户提供其软件应如何工作的示例,并从中获益。c在该领域最知名的举措中,Ward Cunningham 于 2002 年发明了集成测试框架(Fit),允许客户使用 Excel 提供验收标准的示例。Robert C. Martin 在 Fit 的基础上开发了流行的 FitNesse 工具,该工具允许用户使用 wiki 来捕获他们的验收标准。

But acceptance tests don’t have to be written using unit testing tools. Since at least the early 2000s, innovative teams have been asking users to contribute examples of how their software should work, and have been reaping the benefits.c Among the more well-known initiatives in this field, Ward Cunningham invented the framework for integrated tests, or Fit, back in 2002, to allow customers to provide examples of acceptance criteria using Excel. Robert C. Martin built on Fit to develop the popular FitNesse tool, which allows users to use a wiki to capture their acceptance criteria.

实例化需求说明(有时称为 SBE) 描述了使用示例和对话来发现和描述需求的一组实践。d Gojko Adzic在他的同名开创性著作中选择了这个术语作为接触非测试人员的一种方式:他想强调的是,尽管在 ATDD 等术语中使用了“测试”一词,但这些技术实际上是需求发现实践。

Specification by Example (sometimes referred to as SBE) describes the set of practices that use examples and conversation to discover and describe requirements. In his seminal book of the same name,d Gojko Adzic chose this term as a way to reach out to nontesters: he wanted to emphasize that, despite the use of the word “test” in terms like ATDD, these techniques were actually requirements discovery practices.

使用对话和示例来指定您期望系统如何运行是 BDD 的核心部分,我们将在本书的前半部分详细讨论它。在过去十年中,这些技术一直在趋同而不是分化。它们也已经远远超出了简单地先编写测试,拥抱了协作深思熟虑的发现、领域驱动设计、实时文档和许多其他相关实践的力量。如果做得好,它们几乎无法区分。

Using conversation and examples to specify how you expect a system to behave is a core part of BDD, and we’ll discuss it at length in the first half of this book. Over the past decade, these techniques have been converging more than diverging. They have also come a long way beyond simply writing tests first, to embrace the power of collaborative deliberate discovery, domain-driven design, living documentation, and a host of other related practices. When done well, they are virtually indistinguishable from each other.


一个  Matt Wynne,“示例引导开发:xDD 系列的有用抽象?” Cucumberhttps://cucumber.io/blog/example-guided-development/

a  Matt Wynne, “Example-guided development: A useful abstraction for the xDD family?” Cucumber, https://cucumber.io/blog/example-guided-development/.

b  Kent Beck 和 Martin Fowler,《规划极限编程》(Addison-Wesley Professional,2000 年)。

b  Kent Beck and Martin Fowler, Planning Extreme Programming (Addison-Wesley Professional, 2000).

  Johan Andersson、Geoff Bache 和 Peter Sutton,《XP 与验收测试驱动开发:资源优化系统的重写项目》,《计算机科学讲义》 ,第 2675 卷,2003 年。

c  Johan Andersson, Geoff Bache, and Peter Sutton, “XP with Acceptance Test Driven Development: A Rewrite Project for a Resource Optimization System,” Lecture Notes in Computer Science, vol. 2675, 2003.

d  Gojko Adzic,《通过示例规范》(Manning,2011)。

d  Gojko Adzic, Specification by Example (Manning, 2011).

2.3 BDD 原则与实践

2.3 BDD principles and practices

今天BDD 已在全球各种规模的大量组织中以各种不同的方式成功实践。在《实例规范》中,Gojko Adzic为 50 多个此类组织提供了案例研究。在本节中,我们将介绍 BDD 从业者多年来发现的一些有用的一般原则或指南。图 2.2 概述了 BDD 看待世界的方式。BDD 从业者喜欢从确定业务目标开始,然后寻找有助于实现这些目标的功能。在与用户协作时,他们使用具体的示例来说明这些功能。只要有可能,这些示例就会以可执行规范的形式自动生成,这些规范既可以验证软件,又可以提供自动更新的技术和功能文档。BDD 原则也用于编码级别,它们可以帮助开发人员编写质量更高、测试更完善、文档更齐全、更易于使用和维护的代码。

Today BDD is successfully practiced in a large number of organizations of all sizes around the world, in a variety of different ways. In Specification by Example, Gojko Adzic provides case studies for over 50 such organizations. In this section, we’ll look at a number of general principles or guidelines that BDD practitioners have found useful over the years. Figure 2.2 gives a high-level overview of the way BDD sees the world. BDD practitioners like to start by identifying business goals and looking for features that will help deliver these goals. Collaborating with the user, they use concrete examples to illustrate these features. Wherever possible, these examples are automated in the form of executable specifications, which both validate the software and provide automatically updated technical and functional documentation. BDD principles are also used at the coding level, where they help developers write code that’s of higher quality and is better tested, better documented, and easier to use and maintain.

图 2.2 BDD 的主要活动和结果。请注意,这些活动在整个过程中反复、连续地发生;这不是一个单一的线性瀑布式过程,而是您为实现的每个功能练习的一系列活动。

Figure 2.2 The principal activities and outcomes of BDD. Note that these activities occur repeatedly and continuously throughout the process; this isn’t a single linear waterfall-style process, but a sequence of activities that you practice for each feature you implement.

在接下来的章节中,我们将更详细地了解这些原则的工作原理。

In the following sections, we’ll look at how these principles work in more detail.

2.3.1 专注于提供商业价值的功能

2.3.1 Focus on features that deliver business value

作为你已经看到了,需求的不确定性是许多软件项目面临的主要挑战,而当面对需要交付哪些功能的不断变化的理解时,繁重的前期规范并不能起到很好的作用。

As you’ve seen, uncertainty about requirements is a major challenge in many software projects, and heavy upfront specifications don’t work particularly well when confronted with a shifting understanding of what features need to be delivered.

功能是可交付的有形功能,可帮助企业实现其业务目标。例如,假设您在一家正在实施网上银行解决方案银行工作。该项目的业务目标之一可能是“通过为客户提供一种简单便捷的账户管理方式来吸引更多客户”。一些可能有助于实现此目标的功能可能是“在客户账户之间转账”、“将资金转移到另一个国内账户”或“将资金转移到海外账户”。

A feature is a tangible, deliverable piece of functionality that helps the business achieve its business goals. For example, suppose you work in a bank that’s implementing an online banking solution. One of the business goals for this project might be “to attract more clients by providing a simple and convenient way for clients to manage their accounts.” Some features that might help achieve this goal could be “Transfer funds between a client’s accounts,” “Transfer funds to another national account,” or “Transfer funds to an overseas account.”

实践 BDD 的团队不会试图一劳永逸地确定所有需求,而是与最终用户和其他利益相关者进行持续对话,逐步建立对他们应该创建哪些功能的共同理解。用户不会提前设计一个完整的解决方案供开发人员实施,而是解释他们需要从系统中得到什么以及它如何帮助他们实现目标。团队不会不加思索地接受用户的功能请求列表,而是试图了解项目背后的核心业务目标,只提出可以证明支持这些业务目标的功能。这种对交付业务价值的持续关注意味着团队可以更早地交付更有用的功能,并且浪费更少努力。

Rather than attempting to nail down all of the requirements once and for all, teams practicing BDD engage in ongoing conversations with end users and other stakeholders to progressively build a common understanding of what features they should create. Rather than working upfront to design a complete solution for the developers to implement, users explain what they need to get out of the system and how it might help them achieve their objectives. And rather than accepting a list of feature requests from users with no questions asked, teams try to understand the core business goals underlying the project, proposing only features that can be demonstrated to support these business goals. This constant focus on delivering business value means that teams can deliver more useful features earlier and with less wasted effort.

2.3.2 共同指定功能

2.3.2 Work together to specify features

一个复杂的问题,比如发现让客户满意的方法,最好由一群认知多样化的人来解决,他们负责解决问题,自我组织,共同努力解决问题。

A complex problem, like discovering ways to delight clients, is best solved by a cognitively diverse group of people that is given responsibility for solving the problem, self-organizes, and works together to solve it.

—斯蒂芬·丹宁领导者的激进管理指南》(Jossey-Bass,2010 年)

—Stephen Denning, The Leader’s Guide to Radical Management (Jossey-Bass, 2010)

BDD 是一种高度协作的实践,既包括用户与开发团队之间,也包括团队内部。业务分析师、开发人员和测试人员与最终用户一起定义和指定功能,团队成员则从各自的经验和专业知识中汲取灵感。这种方法非常高效。

BDD is a highly collaborative practice, both between users and the development team and within the team itself. Business analysts, developers, and testers work together with end users to define and specify features, and team members draw ideas from their individual experience and know-how. This approach is highly efficient.

在更传统的方法中,当业务分析师简单地将他们对用户需求的理解传达给团队的其他成员时,很容易出现误解和信息丢失的风险。

In a more traditional approach, when business analysts simply relay their understanding of the users’ requirements to the rest of the team, there is a high risk of misinterpretation and lost information.

如果你要求用户写下他们想要的东西,他们通常会给你一组与他们对解决方案的设想相匹配的详细需求。换句话说,用户不会告诉你他们需要什么;相反,他们会为你设计一个解决方案。我见过许多业务分析师陷入同样的​​陷阱,仅仅是因为他们接受过以这种方式编写规范的培训。这种方法的问题在于双重:他们不仅无法从开发团队在软件设计方面的专业知识中受益,而且他们实际上将开发团队绑定到特定的解决方案上,而该解决方案在业务或技术方面可能不是最佳的。此外,开发人员无法利用他们的技术知识来帮助提供技术上更优越的设计,而测试人员直到项目结束才有机会对规范的可测试性发表评论。

If you ask users to write up what they want, they’ll typically give you a set of detailed requirements that matches how they envisage the solution. In other words, users will not tell you what they need; rather, they’ll design a solution for you. I’ve seen many business analysts fall into the same trap, simply because they’ve been trained to write specifications that way. The problem with this approach is twofold: not only will they fail to benefit from the development team’s expertise in software design, but they’re effectively binding the development team to a particular solution, which may not be the optimal one in business or technical terms. In addition, developers can’t use their technical know-how to help deliver a technically superior design, and testers don’t get the opportunity to comment on the testability of the specifications until the end of the project.

例如,“将资金转入海外账户”功能涉及许多用户体验和技术考虑。你如何向客户显示不断变化的汇率?费用何时以及如何计算并显示给客户?你能保证提议的汇率多久?你如何验证使用的汇率是否正确?所有这些考虑都会影响功能的设计、实施和成本,并可能改变业务分析师和业务利益相关者最初设想的解决方案的方式。另一方面,当团队实践 BDD 时,团队成员会对用户的需求达成共识,并在项目过程中产生共同的主人翁意识和参与感。解决方案。

For example, the “Transfer funds to an overseas account” feature involves many user-experience and technical considerations. How can you display the constantly changing exchange rates to the client? When and how are the fees calculated and shown to the client? For how long can you guarantee a proposed exchange rate? How can you verify that the right exchange rate is being used? All these considerations will influence the design, implementation, and cost of the feature and can change the way the business analysts and business stakeholders originally imagined the solution. When teams practice BDD, on the other hand, team members build up a shared appreciation of the users’ needs, as well as a sense of common ownership and engagement in the solution.

2.3.3 拥抱不确定性

2.3.3 Embrace uncertainty

一个BDD 团队知道,无论他们花多长时间编写规范,他们都不会提前知道所有事情。正如我们之前所讨论的,在软件项目中,阻碍开发人员进度的最大因素是了解他们需要构建什么。

A BDD team knows that they won’t know everything upfront, no matter how long they spend writing specifications. As we discussed earlier, the biggest thing slowing developers down in a software project is understanding what they need to build.

BDD 实践者不会在项目开始时就锁定规范,而是假设需求(或者更准确地说,他们对需求的理解)会在整个项目生命周期中不断发展和变化。他们试图从用户和利益相关者那里获得早期反馈,以确保他们走在正确的轨道上,并相应地改变策略,而不是等到项目结束时才知道他们对业务需求的假设是否正确。

Rather than attempting to lock down the specifications at the start of the project, BDD practitioners assume that the requirements, or more precisely, their understanding of the requirements, will evolve and change throughout the life of the project. They try to get early feedback from users and stakeholders to ensure that they’re on track, and change tack accordingly, instead of waiting until the end of the project to see if their assumptions about the business requirements were correct.

通常,了解用户是否喜欢某个功能的最有效方法是尽早构建并向他们展示。考虑到这一点,经验丰富的 BDD 团队会优先考虑那些能够带来价值的功能,这将提高他们对用户真正需要的功能的理解,并帮助他们了解如何最好地构建和交付这些功能特征。

Very often, the most effective way to see if users like a feature is to build it and show it to them as early as possible. With this in mind, experienced BDD teams prioritize the features that will deliver value, will improve their understanding of what features the users really need, and will help them understand how best to build and deliver these features.

2.3.4 用具体的例子来说明特点

2.3.4 Illustrate features with concrete examples

什么时候当一个实践 BDD 的团队决定实现一个功能时,他们会与用户和其他利益相关者一起定义用户期望该功能提供的故事和场景。特别是,用户会帮助定义一组具体的示例来说明该功能的关键结果(见图 2.3)。

When a team practicing BDD decides to implement a feature, they work together with users and other stakeholders to define stories and scenarios of what users expect this feature to deliver. In particular, the users help define a set of concrete examples that illustrate key outcomes of the feature (see figure 2.3).

图2.3 示例在BDD中起着主要作用,帮助每个人更清楚地理解需求。

Figure 2.3 Examples play a primary role in BDD, helping everyone understand the requirements more clearly.

这些示例使用通用词汇,最终用户和开发团队成员都可以轻松理解。它们通常使用您在第 2.2 节中看到的 Given ... When ... Then 符号来表达。例如,一个说明“在客户账户之间转移资金”功能的简单示例可能如下所示:

These examples use a common vocabulary and can be readily understood by both end users and members of the development team. They’re usually expressed using the Given ... When ... Then notation you saw in section 2.2. For instance, a simple example that illustrates the “Transfer funds between a client’s accounts” feature might look like this:

场景:将钱转入储蓄账户
    假设 Tess 有一个 1000 美元的活期账户
    她有一个2000美元的储蓄账户
    当她将 500 美元从活期账户转入储蓄账户时
    那么她的活期账户里应该有 500 美元
    她的储蓄账户里应该有 2500 美元
Scenario: Transferring money to a savings account
    Given Tess has a current account with $1000
    And she has savings account with $2000
    When she transfers $500 from her current account to her savings account
    Then she should have $500 in her current account
    And she should have $2500 in her savings account

示例在 BDD 中起着主要作用,因为它们是传达清晰、准确和无歧义需求的极其有效的方式。事实证明,用自然语言编写的规范是一种非常糟糕的需求传达方式,因为其中存在太多的歧义、假设和误解。示例是克服这些限制并澄清需求的好方法。示例也是探索和扩展知识的好方法。当用户提出功能应如何运行的示例时,项目团队成员通常会要求提供额外的示例来说明极端情况、探索边缘情况或澄清假设。测试人员在这方面特别擅长,这就是为什么他们参与到这个阶段是如此有价值项目。

Examples play a primary role in BDD, simply because they’re an extremely effective way of communicating clear, precise, and unambiguous requirements. Specifications written in natural language are, as it turns out, a terribly poor way of communicating requirements, because there’s so much space for ambiguity, assumptions, and misunderstandings. Examples are a great way to overcome these limitations and clarify the requirements. Examples are also a great way to explore and expand your knowledge. When a user proposes an example of how a feature should behave, project team members often ask for extra examples to illustrate corner cases, explore edge cases, or clarify assumptions. Testers are particularly good at this, which is why it’s so valuable for them to be involved at this stage of the project.

2.3.5 Gherkin 引物

2.3.5 A Gherkin primer

最多我们将在本书中讨论的 BDD 工具使用一种通常称为 Gherkin 的格式,因此在进一步讨论之前,有必要澄清一下这到底是什么。这种格式旨在让业务利益相关者轻松理解,并且易于通过专用的 BDD 工具(如 Cucumber 和 SpecFlow)实现自动化。这样,它既可以记录您的需求,又可以运行您的自动化测试。

Most BDD tools that we’ll look at in this book use a format generally known as Gherkin, so before we go any further, it is worth clarifying just what this is. This format is designed to be both easily understandable for business stakeholders and easy to automate using dedicated BDD tools such as Cucumber and SpecFlow. This way, it both documents your requirements and runs your automated tests.

在 Gherkin 中,与特定功能相关的需求被分组到一个名为功能文件的文本文件中,其中包含该功能的简短描述,后面跟着一些场景,或者功能如何运作的形式化示例。

In Gherkin, the requirements related to a particular feature are grouped into a single text file called a feature file, which contains a short description of the feature, followed by a number of scenarios, or formalized examples of how a feature works.

功能:账户间转账
  为了更有效地管理我的资金
  作为银行客户
  我希望随时在我的账户之间转账
  场景:将钱转入储蓄账户
    假设 Tess 有一个 1000 美元的活期账户
    还有一个 2000 美元的储蓄账户
    当她将 500 美元从活期账户转到储蓄账户时
    那么她的活期账户里应该有 500 美元
    她的储蓄账户里应该有 2500 美元
 
  场景:资金不足时转账
    假设 Tess 有一个 1000 美元的活期账户
    还有一个 2000 美元的储蓄账户
    当她将 1500 美元从活期账户转到储蓄账户时
    然后她应该会收到“资金不足”的错误
    那么她的活期账户里应该有 1000 美元
    她的储蓄账户里应该有 2000 美元
Feature: Transferring money between accounts
  In order to manage my money more efficiently
  As a bank client
  I want to transfer funds between my accounts whenever I need to
  Scenario: Transferring money to a savings account
    Given Tess has a current account with $1000
    And a savings account with $2000.00
    When she transfers $500 from current to savings
    Then she should have $500 in her current account
    And she should have $2500 in her savings account
 
  Scenario: Transferring with insufficient funds
    Given Tess has a current account with $1000
    And a savings account with $2000.00
    When she transfers $1500 from current to savings
    Then she should receive an 'insufficient funds' error
    Then she should have $1000 in her current account
    And she should have $2000 in her Savings account

从这里可以看出,Gherkin 需求用简单的英语表达,但具有特定的结构。每个场景由多个步骤组成,每个步骤都以少数几个关键字之一开头(GivenWhenThenAndBut)。

As can be seen here, Gherkin requirements are expressed in plain English, but with a specific structure. Each scenario is made up of a number of steps, where each step starts with one of a small number of keywords (Given, When, Then, And, But).

场景的自然顺序是给定...当...然后:

The natural order of a scenario is Given ... When ... Then:

  • 给出了场景的先决条件的描述并准备了测试环境。

  • Given describes the preconditions for the scenario and prepares the test environment.

  • 何时描述所测试的动作。

  • When describes the action under test.

  • 然后描述预期结果。

  • Then describes the expected outcomes.

And和But关键字可用于以更易读的方式连接多个 Given、When 或 Then 步骤

The And and But keywords can be used to join several Given, When, or Then steps in a more readable way:

假设她有一个 1000 美元的活期账户
她有一个 2000 美元的储蓄账户
Given she has a current account with $1000
And she has a savings account with $2000

通常可以使用示例表将多个相关场景归类为一个场景。例如,以下场景说明了如何计算不同类型账户的利息:

Several related scenarios can often be grouped into a single scenario using a table of examples. For example, the following scenario illustrates how interest is calculated on different types of accounts:

情景概述:赚取利息
  假设 Tess 有一个 <account-type> 账户,其中有 $<initial-balance>
  <account-type> 账户的利率为 <interest>
  每月利息计算时
  那么她应该赚到$<earnings>
  并且她的 <account-type> 帐户中应该有 $<new-balance>
  例子:
  | 期初余额 | 账户类型 | 利息 | 收益 | 新余额 |
  | 10000 | 当前 | 1.0 | 8.33 | 10008.33 |
  | 10000 | 节省 | 3.0 | 25 | 10025 |
  | 10000 | 超级节省 | 5.0 | 41.67 | 10041.67 |
Scenario Outline: Earning interest
  Given Tess has a <account-type> account with $<initial-balance>
  And the interest rate for <account-type> accounts is <interest>
  When the monthly interest is calculated
  Then she should have earned $<earnings>
  And she should have $<new-balance> in her <account-type> account
  Examples:
  | initial-balance | account-type | interest | earnings | new-balance |
  | 10000           | Current      | 1.0      | 8.33     | 10008.33    |
  | 10000           | Savings      | 3.0      | 25       | 10025       |
  | 10000           | SuperSaver   | 5.0      | 41.67    | 10041.67    |

此场景总共将运行三次,针对示例表中的每一行运行一次。每行中的值都插入占位符变量中,这些变量由<...>符号 ( <account-type><initial-balance>等) 表示。这不仅节省了输入时间,而且使一目了然地了解整个需求变得更容易。

This scenario would be run three times in all, once for each row in the Examples table. The values in each row are inserted into the placeholder variables, which are indicated by the <...> notation (<account-type>, <initial-balance>, etc.). This not only saves typing, but also makes it easier to understand the whole requirement at a glance.

您还可以在步骤本身中使用以下表格符号,以便更简洁地显示测试数据。例如,之前的转账场景可以这样写:

You can also use the following tabular notation within the steps themselves in order to display test data more concisely. For example, the previous money-transfer scenario could have been written like this:

场景:银行内账户间转账
  假设 Tess 有以下帐户:
    | 账户 | 余额 |
    | 当前 | 1000 |
    | 储蓄 | 2000 |
  当她将 500.00 美元从活期账户转入储蓄账户时
  那么她的账户应该是这样的:
    | 账户 | 余额 |
    | 当前 | 500 |
    | 节省 | 2500 |
Scenario: Transferring money between accounts within the bank
  Given Tess has the following accounts:
    | account | balance |
    | current | 1000    |
    | savings | 2000    |
  When she transfers 500.00 from current to savings
  Then her accounts should look like this:
    | account | balance |
    | current | 500     |
    | savings | 2500    |

我们将在第 5 章。

We’ll look at this notation in much more detail in chapter 5.

2.3.6 不要编写自动化测试;编写可执行规范

2.3.6 Don’t write automated tests; write executable specifications

这些故事和示例构成了开发人员构建系统时使用的规范的基础。它们既是验收标准,决定何时完成某个功能,也是开发人员的指导方针,让他们清楚地了解需要构建什么。

These stories and examples form the basis of the specifications that developers use to build the system. They act as both acceptance criteria, determining when a feature is done, and as guidelines for developers, giving them a clear picture of what needs to be built.

验收标准使团队能够客观地判断某个功能是否已正确实现。但是,手动检查每个代码更改会耗时且效率低下。这还会减慢反馈速度,从而减慢开发过程。只要可行,团队就会将这些验收标准转变为自动验收测试,或者更准确地说,转变为可执行规范

Acceptance criteria give the team a way to objectively judge whether a feature has been implemented correctly. But checking this manually for each code change would be time-consuming and inefficient. It would also slow down feedback, which would in turn slow down the development process. Wherever feasible, teams turn these acceptance criteria into automated acceptance tests or, more precisely, into executable specifications.

可执行规范是一种自动化测试,用于说明和验证应用程序如何满足特定业务需求。这些自动化测试作为构建过程的一部分运行,并在应用程序发生更改时运行。这样,它们既可以作为验收测试(确定哪些新功能已完成),也可以作为回归测试(确保新更改不会破坏任何现有功能)(见图 2.4)。

An executable specification is an automated test that illustrates and verifies how the application delivers a specific business requirement. These automated tests run as part of the build process and run whenever a change is made to the application. In this way, they serve both as acceptance tests, determining which new features are complete, and as regression tests, ensuring that new changes haven’t broken any existing features (see figure 2.4).

图 2.4 可执行规范使用整个团队都能理解的通用业务词汇来表达。它们指导开发和测试活动,并生成可供所有人使用的可读报告。

Figure 2.4 Executable specifications are expressed using a common business vocabulary that the whole team can understand. They guide development and testing activities and produce readable reports available to all.

您可以通过编写与每个步骤对应的测试代码来自动化可执行规范。Cucumber 等 BDD 工具会将场景中每个步骤的文本与相应的测试代码进行匹配。例如,这是图 2.4 中场景的第一步:

You can automate an executable specification by writing test code corresponding to each step. BDD tools such as Cucumber will match the text in each step of your scenario to the appropriate test code. For example, this is the first step of the scenario in figure 2.4:

假设 Tess 有一个 1000 美元的活期账户
Given Tess has a current account with $1000

您可以使用 Cucumber 在 Java 中自动执行此步骤,代码如下:

You might automate this step in Java using Cucumber with code like this:

客户端客户端;
 
@Given("{client} 有一个 {accountType} 账户,账户金额为 ${int}")                  
public void setutAccount(客户端客户端,
                         账户类型 账户类型, 
                         int 余额){
    这个.客户端=客户端;
    客户端.打开(BankAccount.ofType(accountType).withBalance(balance));     
}
Client client;
 
@Given("{client} has a {accountType} account with ${int}")                 
public void setutAccount(Client client,
                         AccountType accountType, 
                         int balance) {
    this.client = client;
    client.opens(BankAccount.ofType(accountType).withBalance(balance));    
}

此代码实现的步骤

The step that this code implements

调用本步骤对应的应用程序代码。

Call the application code that corresponds to this step.

当 Cucumber 运行该场景时,它将执行每个步骤,使用基本模式匹配来查找与步骤 1 相关的方法。一旦知道要调用什么方法,它就会提取变量,例如accountTypebalance并执行相应的应用程序代码2.

When Cucumber runs the scenario, it’ll execute each step, using basic pattern matching to find the method associated with step 1. Once it knows what method to call, it’ll extract variables like accountType and balance and execute the corresponding application code 2.

与传统的单元测试或集成测试,或许多 QA 团队习惯的自动化功能测试不同,可执行规范以接近自然语言的方式表达。它们精确地使用用户和开发团队成员先前提出和改进的示例,以及相同的术语和词汇。可执行规范既用于验证,也用于沟通,它们生成的测试报告很容易被参与项目的每个人理解。

Unlike conventional unit or integration tests, or the automated functional tests many QA teams are used to, executable specifications are expressed in something close to natural language. They use precisely the examples that users and development team members proposed and refined earlier on, and the same terms and vocabulary. Executable specifications are about communication as much as they are about validation, and the test reports they generate are easily understandable by everyone involved with the project.

这些可执行规范也成为单一事实来源,为如何实现功能提供参考文档。这使得维护需求变得更加容易。如果规范以 Word 文档或 Wiki 页面的形式存储,就像许多传统项目一样,则对需求的任何更改都需要反映在需求文档以及验收测试和测试脚本中,这会带来很高的不一致风险。对于实践 BDD 的团队来说,需求和可执行规范是同一件事;当需求发生变化时,可执行规范会直接在一个地方更新。我们将在第九章。

These executable specifications also become a single source of truth, providing reference documentation for how features should be implemented. This makes maintaining the requirements much easier. If specifications are stored in the form of a Word document or on a wiki page, as is done for many traditional projects, any changes to the requirements need to be reflected both in the requirements document and in the acceptance tests and test scripts, which introduces a high risk of inconsistency. For teams practicing BDD, the requirements and executable specifications are the same thing; when the requirements change, the executable specifications are updated directly in a single place. We’ll look at this in detail in chapter 9.

2.3.7 这些原则也适用于单元测试

2.3.7 These principles also apply to unit tests

身体缺陷诊断并不止于验收测试。许多核心 BDD 原则和价值观也可以应用于单元测试,这可以帮助开发人员编写更可靠、更易于维护且文档更齐全的高质量代码。

BDD doesn’t stop at the acceptance tests. Many of the core BDD principles and values can also be applied to unit testing, and this helps developers write higher-quality code that’s more reliable, more maintainable, and better documented.

单元测试是描述和验证系统各个组件行为的小型测试。单元测试侧重于系统的内部工作;在现代编程语言中,组件可能是方法或函数。

Unit tests are small tests that describe and verify the behavior of individual components of a system. Unit tests focus on the internal workings of the system; in modern programming languages, a component might be a method or a function.

我们在上一节中看到的可执行规范是使用业务领域的术语和概念编写的。它们旨在让业务人员和最终用户轻松理解。我们可以将这些面向客户的可执行规范称为而单元测试则是由开发人员编写的,为开发人员服务。编写良好的单元测试会描述和记录低级组件行为,与可执行规范文档非常相似,并描述用户如何与系统交互。

The executable specifications we saw in the previous section are written using terms and concepts from the business domain. They are designed to be easily understood by businesspeople and end users. We might call these customer-facing executable specifications. Unit tests, on the other hand, are written by developers, for developers. Well-written unit tests describe and document low-level component behavior, much in the same way as executable specifications documents, and describe how users interact with the system.

实践 BDD 的开发人员通常使用由外而内的方法当他们实现一项功能时,他们从验收标准开始,然后逐步构建使这些验收标准通过所需的一切。验收标准定义了预期结果,开发人员的工作是编写产生这些结果的代码。这是一种非常高效、专注的工作方式。正如任何功能只有在它有助于确定的业务目标时才会实现一样,任何代码只有在它有助于通过验收测试并因此有助于实现一项功能时才会被编写。

Developers practicing BDD typically use an outside-in approach. When they implement a feature, they start from the acceptance criteria and work down, building whatever is needed to make those acceptance criteria pass. The acceptance criteria define the expected outcomes, and the developer’s job is to write the code that produces those outcomes. This is a very efficient, focused way of working. Just as no feature is implemented unless it contributes to an identified business goal, no code is written unless it contributes to making an acceptance test pass, and therefore to implementing a feature.

但它并不止于此。在编写任何代码之前,BDD 开发人员都会推断这段代码实际上应该做什么,并以低级或面向开发人员的可执行规范的形式表达这一点。开发人员不会考虑为特定类编写单元测试,而是考虑编写技术规范来描述应用程序应该如何运行,例如它应该如何响应某些输入或在特定情况下应该做什么。这些低级规范自然而然地从高级验收标准中流露出来,并帮助开发人员在提供高级功能的背景下设计和记录应用程序代码(见图 2.5)。

But it doesn’t stop there. Before writing any code, a BDD developer will reason about what this code should actually do and express this in the form of a low-level, or developer-facing, executable specification. The developer won’t think in terms of writing unit tests for a particular class, but of writing technical specifications describing how the application should behave, such as how it should respond to certain inputs or what it should do in a given situation. These low-level specifications flow naturally from the high-level acceptance criteria and help developers design and document the application code in the context of delivering high-level features (see figure 2.5).

图 2.5 以单元测试形式编写的低级规范自然地从高级规范中流出。

Figure 2.5 Low-level specifications, written as unit tests, flow naturally from the high-level specifications.

例如,图2.5中的步骤定义代码涉及创建一个新账户:

For example, the step definition code in figure 2.5 involves creating a new account:

@Given("{client} 有一个 {accountType} 帐户,金额为 ${int}")    
公共无效设置帐户(客户端客户端,
                         账户类型 账户类型, 
                         int 余额){
    这个.客户端=客户端;
    客户端.打开(BankAccount.ofType(accountType).withBalance(balance));     
}
@Given("{client} has a {accountType} account with ${int}")    
public void setupAccount(Client client,
                         AccountType accountType, 
                         int balance) {
    this.client = client;
    client.opens(BankAccount.ofType(accountType).withBalance(balance));    
}

创建给定类型和给定初始余额的新帐户。

Create a new account of a given type and with given initial balance.

这导致开发人员编写面向开发人员的低级规范来设计Account使用 Junit 5 编写的面向开发人员的可执行规范示例可能如下所示:

This leads the developer to write low-level, developer-facing specifications to design the Account class. An example of a developer-facing executable specification written using Junit 5 might look like this:

@DisplayName("创建新银行账户时")
创建新帐户时类 {
 
    @DisplayName("新帐户应该有初始余额")
    @测试
    无效的新帐户余额(){
        银行账户帐户 = 银行账户.ofType(帐户类型.储蓄)
                                         .余额(100);
        断言(account.getBalance())。是否等于(100.0);
    }
}x
@DisplayName("When creating a new bank account")
class WhenCreatingANewAccount {
 
    @DisplayName("A new account should have an initial balance")
    @Test
    void newAccountBalance() {
        BankAccount account = BankAccount.ofType(AccountType.Savings)
                                         .withBalance(100);
        assertThat(account.getBalance()).isEqualTo(100.0);
    }
}x

根据您使用的平台,您还可以使用其他单元测试工具(例如 NUnit 或 Mocha)或更专业的 BDD 工具(例如 RSpec)编写此规范。

Depending on the platform you are working with, you could also write this specification using other unit-testing tools, such as NUnit or Mocha, or more specialized BDD tools such as RSpec.

此类可执行规范与传统的单元测试类似,但它们的编写方式既能传达代码的意图,又能提供代码使用示例。以这种方式编写低级可执行规范有点像编写详细的设计文档,其中包含大量示例,但使用的工具对开发人员来说既简单又有趣。

Executable specifications like this are similar to conventional unit tests, but they’re written in a way that both communicates the intent of the code and provides a worked example of how the code should be used. Writing low-level executable specifications this way is a little like writing detailed design documentation, with lots of examples, but using a tool that’s easy and even fun for developers.

航天飞机的单元测试

Unit testing the space shuttle

优秀的开发人员很早就知道单元测试的重要性。IBM 联邦系统部门团队在 70 年代末为 NASA 航天飞机编写中央航空电子软件时充分意识到了单元测试的重要性。他们采用的单元测试方法是使用需求和业务提供的示例来设计单元测试,这种方法给人一种令人惊讶的现代感:

Good developers have known the importance of unit testing for a very long time. The IBM Federal Systems Division team was fully aware of their importance when they wrote the central avionics software for NASA’s space shuttle in the late seventies. The approach they took to unit testing, where the unit tests were designed using the requirements and with examples provided by the business, has a surprisingly modern feel to it:

   

   

在开发活动期间,进行了特定测试,以确保数学方程式和逻辑路径提供预期的结果。检查这些算法和逻辑路径的准确性,并在可能的情况下与外部来源的结果和系统设计规范 (SDS) 进行比较

During the development activity, specific testing was done to ensure that the mathematical equations and logic paths provided the results expected. These algorithms and logic paths were checked for accuracy and, where possible, compared against results from external sources and against the system design specification (SDS).”a

   

   


一个  William A. Madden 和 Kyle Y. Rone,《设计、开发、集成:航天飞机主飞行软件系统》,《ACM 通讯》(1984 年 9 月)。

a  William A. Madden and Kyle Y. Rone, “Design, Development, Integration: Space Shuttle Primary Flight Software System,” Communications of the ACM (September 1984).

从技术层面上讲,这种方法鼓励简洁、模块化的设计,模块之间具有明确定义的交互(或者说 API,如果你更喜欢一个更专业的术语)。它还能产生可靠、准确且非常好的代码已测试。

At a more technical level, this approach encourages a clean, modular design with well-defined interactions (or APIs, if you prefer a more technical term) between the modules. It also results in code that’s reliable, accurate, and extremely well tested.

2.3.8 提供动态文档

2.3.8 Deliver living documentation

可执行规范生成的报告不仅仅是面向开发人员的技术报告,而且实际上成为面向整个团队的产品文档的一种形式,以用户熟悉的词汇来表达。此文档始终是最新的,几乎不需要手动维护。它是从应用程序的最新版本自动生成的。每个应用程序功能都以易读的术语描述,并通过几个关键示例进行说明。对于 Web 应用程序,这种动态文档通常还包括每个功能的应用程序屏幕截图。

The reports produced by executable specifications aren’t simply technical reports for developers but effectively become a form of product documentation for the whole team, expressed in a vocabulary familiar to users. This documentation is always up to date and requires little or no manual maintenance. It’s automatically produced from the latest version of the application. Each application feature is described in readable terms and is illustrated by a few key examples. For web applications, this sort of living documentation often also includes screenshots of the application for each feature.

经验丰富的团队会整理这些文档,使其易于阅读,并方便项目中的每个人使用(见图 2.6)。开发人员可以查阅它以了解现有功能的工作原理。测试人员和业务分析师可以看到他们指定的功能是如何实现的。产品所有者和项目经理可以使用摘要视图来判断项目的当前状态,查看进度,并决定哪些功能可以投入生产。用户甚至可以使用它来查看应用程序可以做什么以及它是如何工作的。

Experienced teams organize this documentation so that it’s easy to read and easy for everyone involved in the project to use (see figure 2.6). Developers can consult it to see how existing features work. Testers and business analysts can see how the features they specified have been implemented. Product owners and project managers can use summary views to judge the current state of the project, view progress, and decide what features can be released into production. Users can even use it to see what the application can do and how it works.

图 2.6 组织良好的动态文档可以概述项目的状态,并详细描述特性。

Figure 2.6 Well-organized living documentation can give an overview of the state of a project, as well as describe features in detail.

正如自动化验收标准为整个团队提供了出色的文档一样,低级可执行规范也为其他开发人员提供了出色的技术文档。此文档始终是最新的,维护成本低,包含工作代码示例,并表达了每个文档背后的意图规格。

Just as automated acceptance criteria provide great documentation for the whole team, low-level executable specifications also provide excellent technical documentation for other developers. This documentation is always up to date, is cheap to maintain, contains working code samples, and expresses the intent behind each specification.

2.3.9 使用动态文档支持正在进行的维护工作

2.3.9 Use living documentation to support ongoing maintenance work

动态文档和可执行规范的好处并不会随着项目结束而停止。使用这些实践开发的项目也更容易维护,维护成本也更低。

The benefits of living documentation and executable specifications don’t stop at the end of the project. A project developed using these practices is also significantly easier and less expensive to maintain.

根据 Robert L. Glass(引用其他来源)的说法,维护占软件成本的 40% 到 80%。尽管许多团队发现,当他们采用 BDD 等技术时,缺陷数量会大幅下降,但缺陷仍然可能发生。持续的增强也是任何软件应用程序的自然组成部分。6

According to Robert L. Glass (quoting other sources), maintenance represents between 40% and 80% of software costs. Although many teams find that the number of defects drops dramatically when they adopt techniques like BDD, defects can still happen. Ongoing enhancements are also a natural part of any software application.6

在许多组织中,当一个项目投入生产时,它会被交给另一个团队进行维护工作。参与此维护工作的开发人员通常没有参与项目的开发,需要从头开始学习代码库。实用、相关且最新的功能和技术文档使这项任务变得容易得多。

In many organizations, when a project goes into production, it’s handed over to a different team for maintenance work. The developers involved in this maintenance work have often not been involved in the project’s development and need to learn the code base from scratch. Useful, relevant, and up-to-date functional and technical documentation makes this task a great deal easier.

BDD 开发流程产生的自动化文档对于提供维护团队需要的文档类型大有裨益,因为维护团队需要这些文档才能提高工作效率。高级可执行规范可帮助新开发人员了解应用程序的业务目标和流程。单元测试级别的可执行规范提供了特定功能如何实现的详细工作示例。

The automated documentation that comes out of a BDD development process can go a long way toward providing the sort of documentation maintenance teams need in order to be effective. The high-level executable specifications help new developers understand the business goals and flow of the application. Executable specifications at the unit-testing level provide detailed worked examples of how particular features have been implemented.

从事 BDD 项目的维护开发人员发现,当他们需要进行更改时,更容易知道从哪里开始。良好的可执行规范提供了大量如何正确测试应用程序的示例,而维护更改通常涉及按照类似的思路编写新的可执行规范或修改现有规范。

Maintenance developers working on a BDD project find it easier to know where to start when they need to make a change. Good executable specifications provide a wealth of examples of how to test the application correctly, and maintenance changes will generally involve writing a new executable specification along similar lines or modifying an existing one.

维护性变更对现有代码的影响也更容易评估。当开发人员进行更改时,可能会导致现有的可执行规范被破坏,而当这种情况发生时,通常有两个可能的原因:

The affect of maintenance changes on existing code is also easier to assess. When a developer makes a change, it may cause existing executable specifications to break, and when this happens, there are usually two possible causes:

  • 损坏的可执行规范可能不再反映新的业务需求。在这种情况下,可以更新或删除可执行规范(如果不再相关)。

  • The broken executable specification may no longer reflect the new business requirements. In this case, the executable specification can be updated or (if it’s no longer relevant) deleted.

  • 代码更改破坏了现有要求。这是新代码中的一个错误,需要修复。

  • The code change has broken an existing requirement. This is a bug in the new code that needs to be fixed.

可执行规范并不是解决传统技术文档问题的灵丹妙药。它们不能保证总是有意义或相关的;这需要实践和纪律。通常还需要其他技术、架构和功能文档来完成整个过程。但是,如果编写和组织得当,可执行规范比传统的方法。

Executable specifications are not a magical solution to the traditional problems of technical documentation. They aren’t guaranteed to always be meaningful or relevant; this requires practice and discipline. Other technical, architectural, and functional documentation is often required to complete the picture. But when they’re written and organized well, executable specifications provide significant advantages over conventional approaches.

2.4 BDD 的好处

2.4 Benefits of BDD

在前面的部分中,我们研究了 BDD 是什么样子,并讨论了它能带来什么。现在让我们更详细地介绍一下采用 BDD 的组织可以期待的一些关键业务优势。

In the previous sections, we examined what BDD looks like and discussed what it brings to the table. Now let’s run through some of the key business benefits that an organization adopting BDD can expect in more detail.

2.4.1 减少废弃物

2.4.1 Reduced waste

身体缺陷诊断就是将开发工作重点放在发现和交付能够提供商业价值的功能上,避免那些没有商业价值的功能。当团队构建的功能与项目背后的业务目标不一致时,业务的努力就白费了。同样,当团队编写了业务需要的功能,但对业务没有用处时,团队将需要重新设计该功能以满足要求,从而导致更多的浪费。BDD 可以帮助团队专注于与业务目标一致的功能,从而避免这种浪费。

BDD is all about focusing the development effort on discovering and delivering the features that will provide business value and avoiding those that don’t. When a team builds a feature that’s not aligned with the business goals underlying the project, the effort is wasted for the business. Similarly, when a team writes a feature that the business needs, but in a way that’s not useful to the business, the team will need to rework the feature to fit the bill, resulting in more waste. BDD helps avoid this sort of wasted effort by helping teams focus on features that are aligned with business goals.

BDD 还可以通过向用户提供更快、更有用的反馈来减少浪费的努力。这有助于团队尽早做出改变,而不是之后。

BDD also reduces wasted effort by enabling faster, more useful feedback to users. This helps teams make changes sooner rather than later.

2.4.2 降低成本

2.4.2 Reduced costs

减少浪费的直接结果是降低成本。通过专注于构建具有可证明商业价值的功能(构建正确的软件),而不是在价值不大的功能上浪费精力,您可以降低向用户提供可行产品的成本。通过提高应用程序代码的质量(正确构建软件),您可以减少错误的数量,从而减少修复这些错误的成本,以及与这些错误造成的延迟相关的成本原因。

The direct consequence of this reduced waste is reduced costs. By focusing on building features with demonstrable business value (building the right software), and not wasting effort on features of little value, you can reduce the cost of delivering a viable product to your users. And by improving the quality of the application code (building the software right), you reduce the number of bugs, and therefore the cost of fixing these bugs, as well as the cost associated with the delays these bugs would cause.

2.4.3 更轻松、更安全的变更

2.4.3 Easier and safer changes

身体缺陷诊断使更改和扩展应用程序变得相当容易。使用利益相关者熟悉的术语从可执行规范生成动态文档。这使得利益相关者更容易理解应用程序的实际功能。低级可执行规范还充当开发人员的技术文档,使他们更容易理解现有代码库并进行自己的更改。最后,但肯定不是最不重要的一点,BDD 实践产生了一套全面的自动验收和单元测试,这降低了因对应用程序的任何新更改而导致的回归风险应用。

BDD makes it considerably easier to change and extend your applications. Living documentation is generated from the executable specifications using terms that stakeholders are familiar with. This makes it much easier for stakeholders to understand what the application actually does. The low-level executable specifications also act as technical documentation for developers, making it easier for them to understand the existing code base and to make their own changes. Last, but certainly not least, BDD practices produce a comprehensive set of automated acceptance and unit tests, which reduces the risk of regressions caused by any new changes to the application.

2.4.4 更快的发布

2.4.4 Faster releases

这些全面的自动化测试也大大加快了发布周期。测试人员不再需要在每次发布新版本之前进行长时间的手动测试。相反,他们可以使用自动化验收测试作为起点,将时间更高效地花在探索性测试和其他非平凡的测试上手动的測試。

These comprehensive automated tests also speed up the release cycle considerably. Testers are no longer required to carry out long manual testing sessions before each new release. Instead, they can use the automated acceptance tests as a starting point and spend their time more productively and efficiently on exploratory tests and other nontrivial manual tests.

2.5 BDD 的缺点和潜在挑战

2.5 Disadvantages and potential challenges of BDD

尽管尽管 BDD 的好处很多,但将 BDD 引入组织并非总是一帆风顺。在本节中,我们将介绍几种引入 BDD 可能更具挑战性的情况。

While its benefits are significant, introducing BDD into an organization isn’t always without its difficulties. In this section, we’ll look at a few situations where introducing BDD can be more of a challenge.

2.5.1 BDD 需要较高的业务参与和协作

2.5.1 BDD requires high business engagement and collaboration

身体缺陷诊断实践基于对话和反馈。事实上,这些对话推动并建立了团队对需求的理解,以及如何根据这些需求提供业务价值。如果利益相关者不愿意或无法参与对话和协作,或者他们等到项目结束后才给出任何反馈,那么就很难充分发挥病态缺陷。

BDD practices are based on conversation and feedback. Indeed, these conversations drive and build the team’s understanding of the requirements and of how they can deliver business value based on these requirements. If stakeholders are unwilling or unable to engage in conversations and collaboration, or they wait until the end of the project before giving any feedback, it will be hard to draw the full benefits of BDD.

2.5.2 BDD 在敏捷或迭代环境中效果最佳

2.5.2 BDD works best in an Agile or iterative context

身体缺陷诊断需求分析实践假设很难(甚至不可能)预先完全定义需求,并且这些需求会随着团队(和利益相关者)对项目的了解越来越多而演变。这种方法自然更符合敏捷或迭代项目方法论。

BDD requirements-analysis practices assume that it’s difficult, if not impossible, to define the requirements completely upfront and that these will evolve as the team (and the stakeholders) learn more about the project. This approach is naturally more in line with an Agile or iterative project methodology.

2.5.3 BDD 在孤岛中无法很好地发挥作用

2.5.3 BDD doesn’t work well in a silo

在许多大型组织中,孤立的开发方法仍然是常态。详细的规范由业务分析师编写,然后交给通常在异地或海外的开发团队。同样,测试也委托给另一个完全独立的 QA 团队。在这样的组织中,仍然可以在编码级别实践 BDD,开发团队仍然可以期待代码质量的显着提高、更好的设计、更易于维护的代码和更少的缺陷。但是,业务分析师团队和开发人员之间缺乏互动将使使用 BDD 实践逐步澄清和理解实际需求变得更加困难。

In many larger organizations, a siloed development approach is still the norm. Detailed specifications are written by business analysts and then handed off to development teams that are often offsite or offshore. Similarly, testing is delegated to another, totally separate, QA team. In organizations like this, it’s still possible to practice BDD at a coding level, and development teams will still be able to expect significant increases in code quality, better design, more maintainable code, and fewer defects. But the lack of interaction between the business analyst teams and the developers will make it harder to use BDD practices to progressively clarify and understand the real requirements.

同样,孤立的测试团队也可能是一个挑战。如果 QA 团队等到项目结束才介入,或者孤立地介入,他们就会错过早期满足需求的机会,从而浪费精力修复本来可以更早发现并更轻松地修复的问题。如果 QA 团队参与定义并可能自动化验收标准,那么自动化验收标准也会更加有益。场景。

Similarly, siloed testing teams can be a challenge. If the QA team waits until the end of the project to intervene, or does so in isolation, they’ll miss their chance to contribute to requirements earlier on, which results in wasted effort spent fixing problems that could have been found earlier and fixed more easily. Automating the acceptance criteria is also much more beneficial if the QA team participates in defining, and possibly automating, the scenarios.

2.5.4 编写不佳的测试可能导致更高的测试维护成本

2.5.4 Poorly written tests can lead to higher test-maintenance costs

创建自动验收测试,特别是针对复杂的 Web 应用程序,需要一定的技能,许多开始使用 BDD 的团队发现这是一项重大挑战。事实上,如果测试没有经过精心设计,没有正确的抽象和表达水平,它们就会面临脆弱的风险。如果有大量编写不当的测试,它们肯定会很难维护。许多组织已经成功地为复杂的 Web 应用程序实施了自动验收测试,但要做到这一点需要专业知识和经验。我们将在后面的章节中介绍执行此操作的技术书。

Creating automated acceptance tests, particularly for complex web applications, requires a certain skill, and many teams starting to use BDD find this a significant challenge. Indeed, if the tests aren’t carefully designed, with the right levels of abstraction and expressiveness, they run the risk of being fragile. And if there are a large number of poorly written tests, they’ll certainly be hard to maintain. Plenty of organizations have successfully implemented automated acceptance tests for complex web applications, but it takes know-how and experience to get it right. We’ll look at techniques for doing this later on in the book.

概括

Summary

  • BDD 最初被认为是一种更轻松地教授 TDD 的方法。TDD 是一种设计策略,开发人员在编写实际代码之前,以单元测试的形式编写描述其代码预期行为的低级规范。这有助于他们设计和实现更精确、更高质量的代码。

  • BDD was originally conceived as a way to teach TDD—a design strategy where developers write low-level specifications describing the expected behavior of their code, in the form of unit tests, before they write the actual code—more easily. This helps them design and implement more precise, higher-quality code.

  • 从业者很快发现,BDD 原则也可以应用于需求发现和业务分析。

  • Practitioners quickly found that the principles of BDD can also be applied to requirements discovery and business analysis.

  • BDD 从业者采用自上而下的方法实现功能,以验收标准为目标,并使用以可执行规范形式编写的单元测试来描述每个组件的行为。

  • BDD practitioners implement features with a top-down approach, using the acceptance criteria as goals and describing the behavior of each component with unit tests written in the form of executable specifications.

  • 可以使用 Cucumber 或 SpecFlow 等工具自动化可执行规范,以生成自动回归测试和最新的功能文档。

  • Executable specifications can be automated using tools like Cucumber or SpecFlow to produce both automated regression tests and up-to-date functional documentation.

  • BDD 的主要好处包括集中精力提供有价值的功能、减少浪费的精力和成本、使更改更容易、更安全、以及加快发布过程。

  • The main benefits of BDD include focusing efforts on delivering valuable features, reducing wasted effort and costs, making it easier and safer to make changes, and accelerating the release process.

在下一章中,我们将快速了解 BDD 在实践中是什么样子的,从需求分析到自动化单元和验收测试以及功能测试覆盖率報告。

In the next chapter, we’ll take a flying tour of what BDD looks like in practice, all the way from requirements analysis to automated unit and acceptance tests and functional test coverage reports.


1  Daniel Terhorst-North,“介绍 BDD”,http://dannorth.net/introducing-bdd/

1  Daniel Terhorst-North, “Introducing BDD,” http://dannorth.net/introducing-bdd/.

2  Kent Beck,《测试驱动开发:示例》(Addison-Wesley Professional,2002 年)。

2  Kent Beck, Test-Driven Development: By Example (Addison-Wesley Professional, 2002).

3  Rod Hilton,“通过将面向对象质量指标应用于开源项目来定量评估测试驱动开发”(博士论文,里吉斯大学,2009 年),http://www.rodhilton.com/files/tdd_thesis.pdf

3  Rod Hilton, “Quantitatively Evaluating Test-Driven Development by Applying Object-Oriented Quality Metrics to Open Source Projects” (PhD thesis, Regis University, 2009), http://www.rodhilton.com/files/tdd_thesis.pdf.

4  Nachiappan Nagappan、E. Michael Maximilien、Thirumalesh Bhat 和 Laurie Williams,“通过测试驱动开发实现质量改进:四个工业团队的成果和经验”,https://www.microsoft.com/en-us/research/wp-content/uploads/2009/10/Realizing-Quality-Improvement-Through-Test-Driven-Development-Results-and-Experiences-of-Four-Industrial-Teams-nagappan_tdd.pdf

4  Nachiappan Nagappan, E. Michael Maximilien, Thirumalesh Bhat, and Laurie Williams, “Realizing quality improvement through test driven development: results and experiences of four industrial teams,” https://www.microsoft.com/en-us/research/wp-content/uploads/2009/10/Realizing-Quality-Improvement-Through-Test-Driven-Development-Results-and-Experiences-of-Four-Industrial-Teams-nagappan_tdd.pdf.

5  Eric Evans,《领域驱动设计》(Addison-Wesley Professional,2003 年)。

5  Eric Evans, Domain Driven Design (Addison-Wesley Professional, 2003).

6  Robert L. Glass,《软件工程的事实与谬误》(Addison-Wesley Professional,2002 年)。

6  Robert L. Glass, Facts and Fallacies of Software Engineering (Addison-Wesley Professional, 2002).

3 BDD:旋风之旅

3 BDD: The whirlwind tour

本章封面

This chapter covers

  • 对 BDD 实践的端到端演练
  • An end-to-end walkthrough of BDD practices in action
  • 发现特征并通过故事和例子描述它们
  • Discovering features and describing them through stories and examples
  • 使用可执行规范来详细指定功能
  • Using executable specifications to specify features in detail
  • 使用低级 BDD 实现功能
  • Using low-level BDD to implement features
  • 使用 BDD 测试结果作为动态文档并支持持续维护
  • Using BDD test results as living documentation and to support ongoing maintenance

在本章中,我们将通过一个具体示例来说明 BDD 如何在实际项目中发挥作用。正如您在上一章中看到的,BDD 涉及开发团队在整个项目中与客户进行对话,使用示例来建立对业务真正需求的更具体、更清晰的理解。您可以以可执行形式编写规范,可用于定义软件需求、推动其实施并验证您交付的产品。您还可以在更高级别的需求分析期间应用这些技术,帮助您专注于真正为业务增加价值的应用程序功能和特性。

In this chapter, we’ll look at a concrete example of how BDD might work on a real-world project. As you saw in the previous chapter, BDD involves the development team engaging in conversations with the customer throughout the project, using examples to build up a more concrete and less ambiguous understanding of what the business really needs. You write specifications in an executable form that you can use to define software requirements, drive their implementation, and verify the product you deliver. You can also apply these techniques during more high-level requirements analysis, helping you focus on the capabilities and features of the application that will genuinely add value to the business.

此实践的一个关键部分是定义场景,即特定功能或故事如何运作的具体示例。这些场景将帮助您验证和扩展对问题的理解,它们也是极好的沟通工具。它们是验收标准的基础,然后您可以以自动验收测试的形式将其集成到构建过程中。结合自动验收测试,这些示例可以指导开发过程,帮助设计人员准备有效且实用的用户界面设计,并帮助开发人员发现他们需要实现的底层行为以提供所需的功能。

A key part of this practice involves defining scenarios, or concrete examples of how a particular feature or story works. These scenarios will help you to validate and extend your understanding of the problem, and they’re also an excellent communication tool. They act as the foundation of the acceptance criteria, which you then integrate into the build process in the form of automated acceptance tests. In conjunction with the automated acceptance tests, these examples guide the development process, helping designers to prepare effective and functional user interface designs and assisting developers to discover the underlying behaviors that they’ll need to implement to deliver the required features.

在本章的其余部分,我们将研究此过程的实际示例。我们将涉及整个开发周期的各个方面,从业务分析到实现、测试和维护代码。本章的目的是让您了解该方法和所涉及的一些技术,而不是提供任何特定技术堆栈的完整工作示例,但我们将深入介绍足够的技术细节以供您跟进。在接下来的章节中,我们将更详细地介绍本章中涵盖的每个主题以及许多其他主题。

In the rest of this chapter, we’ll look at a practical example of this process in action. We’ll touch on aspects of the whole development cycle, from business analysis to implementing, testing, and maintaining the code. The aim of this chapter is to give you an idea of the approach and some of the technologies involved, rather than to provide a full working example of any particular technology stack, but we’ll go into enough technical detail for you to follow along. In the chapters that follow, we’ll look at each of the topics covered in this chapter, and many others, in much more detail.

3.1 BDD 流程

3.1 The BDD flow

一个可视化 BDD 流程的一个好方法是将其分解为需求从模糊想法到实现功能所经历的各个阶段或步骤。在每个阶段,团队都会执行不同的活动,以加深对需要交付的内容的理解,并确保他们交付的内容符合要求。

A good way to visualize the BDD process is to break it down into the various stages or steps that a requirement goes through in its journey from vague idea to implemented feature. At each stage, teams perform different activities to build up their understanding of what they need to deliver and to be sure that what they do deliver fits the bill.

图 3.1 BDD 团队的关键活动

Figure 3.1 Key activities of a BDD team

图 3.1 直观地展示了这一流程。关键阶段概述如下:

We can see this flow visualized in figure 3.1. The key phases are outlined here:

  1. 推测— 团队与商人进行对话,以确定和了解高层业务目标,并确定有助于我们实现这些目标的关键特性。

  2. Speculate—Where the team has conversations with businesspeople to identify and understand high-level business goals and identify the key features that will help us deliver these goals.

  3. 说明——团队成员通过讨论业务规则和用户旅程的具体示例,对特定功能建立更深入的理解。

  4. Illustrate—Where team members build a deeper understanding of a specific feature, through conversations about concrete examples of business rules and user journeys.

  5. 制定——团队成员将关键示例转换为可执行规范,使用业务人员可读且可作为自动化测试执行的符号。

  6. Formulate—Where team members transform key examples into executable specifications, using a notation that is both readable for businesspeople and that can be executed as automated tests.

  7. 自动化——开发人员和测试人员将这些可执行规范转变为自动化验收测试,并使用这些自动化验收测试来推动开发过程。

  8. Automate—Where developers and testers turn these executable specifications into automated acceptance tests and use these automated acceptance tests to drive the development process.

  9. 证明— 通过的自动验收测试可作为功能已正确实现的证据,并可作为说明当前功能及其工作原理的文档。团队可在此验证功能是否按要求完成。

  10. Demonstrate—Where the passing automated acceptance tests act as evidence that a feature has been correctly implemented and as documentation illustrating the current features and how they work. This is where the team verifies that a feature does what was asked of it.

  11. 证实— 团队和企业了解这些功能在现实世界中的表现如何,以及它们是否实现了所承诺的商业价值。

  12. Validate—Where the team and the business see how the features fare in the real world and whether they deliver the business value they promised.

我们将在本书的其余部分详细讨论每个阶段。让我们在实践的背景下看看这个过程例子。

We will look at each of these phases in detail in the rest of this book. Let’s look at this process in the context of a practice example.

3.2 推测:确定业务价值和特征

3.2 Speculate: Identifying business value and features

特蕾西在一家大型政府公共交通部门工作。她被要求领导一个小团队开发一款应用程序,为繁忙的通勤者提供火车时刻表数据以及有关延误、轨道工作等的实时更新。她决定从头到尾应用 BDD 实践。通过跟踪她的旅程,我们将了解 BDD 在实际项目中的工作原理。图 3.2 显示了她的团队将要处理的铁路网络。让我们首先仔细看看项目目标。

Tracy works for a large, government public-transport department. She has been asked to lead a small team in building an application that will provide train timetable data and real-time updates about delays, track work, and so on to busy commuters. She has decided to apply BDD practices from start to finish. By following her journey, we will get an idea of how BDD works in real-world projects. Figure 3.2 illustrates the rail network her team will be working with. Let’s start by taking a closer look at the project goals.

图 3.2 悉尼铁路网部分

Figure 3.2 Part of the Sydney rail network

3.2.1 确定业务目标

3.2.1 Identifying business objectives

您的团队中每个成员是否都能清楚地表达出项目的业务目标?他们能否用几句简单的句子说明他们正在解决的业务问题是什么?令人惊讶的是,很多团队都做不到这一点。

Can each member of your team articulate the business objectives of your project? Can they state, in a few simple sentences, what business problem they are solving? It is surprising to see for how many teams this is not the case.

一个对目标有深刻共识的团队将能够更好地协调他们的工作,并更有效地应对变化或意外障碍。例如,如果他们因技术原因无法提供某项特定功能,或者他们发现实施该功能的成本比预期高得多,他们可以考虑可能实现相同业务成果的替代方案。

A team with a deep, common understanding of their goals will be able to better align their efforts, and also react to change or unexpected hurdles more effectively. For example, if they cannot deliver a particular feature for a technical reason, or they find it would be much more expensive to implement than expected, they can consider alternatives that might achieve the same business outcome.

对工作价值有清晰愿景的团队往往更有动力、更投入。他们觉得自己是大局的一部分。所有这些都有助于确保整个项目实现其目标。

Teams with a clear vision of the value of their work also tend to be more motivated and engaged. They feel part of the bigger picture. All this goes a long way toward ensuring that the project as a whole meets its objectives.

因此,Tracy 的首要任务是确保每个人都清楚项目目标。在项目初期,她将项目发起人和团队成员带到一个大房间讨论这些目标。据项目发起人介绍,项目目标大致如下:“我们想开发一款应用程序,让通勤者可以在线规划行程。”

So, Tracy’s first task is to make sure that everyone is clear about the project goals. Early on in the project, she brings the project sponsor and her team members into a large room to discuss these goals. According to the project sponsor, the project goal goes something like this: “We want to build an application that lets commuters plan their journeys online.”

该部门的使命宣言之一是让公共交通对旅行者更具吸引力、更高效。通勤者不喜欢等待;他们希望尽量减少在公共交通上浪费的时间。根据客户调查,通勤者最大的痛点是缺少换乘。他们不知道应该乘坐哪趟列车,也不想在站台上站太久。

Part of the department’s mission statement is to make public transport more attractive and more efficient for travelers. Commuters don’t like waiting; they want to minimize the unproductive time they have to spend in public transport. And according to customer research, a big pain point for commuters is missing connections. They have trouble knowing what trains they should take and want to avoid standing on platforms for too long.

但该目标并没有真正涵盖这些细节。它没有解释通勤者为何会使用该应用程序,以及该应用程序如何使他们受益。因此,经过一番讨论,该团队与业务赞助商就以下项目愿景声明达成了一致:“该应用程序将帮助普通通勤者在一年内将平均出行时间缩短 10%,让他们能够更有效地规划行程。”

But the goal doesn’t really capture any of these details. It doesn’t explain why commuters might use the application, and how it might benefit them. So, after a bit of discussion, the team agreed with the business sponsors on the following project vision statement: “The application will help to reduce average travel time for regular commuters by 10% within a year, by allowing them to plan their journeys more effectively.”

现在,团队为目标设定了一个数字,使其可以衡量。所有通勤者都有一张旅行卡,在旅行开始和结束时使用,因此平均旅行时间是可以相当容易衡量的。这可以帮助团队了解他们的目标,也可以帮助组织了解应用程序是否兑现了承诺。

Now the team has put a number on the goal, which makes it measurable. All commuters have a travel card that they use at the start and end of their trips, so average trip time is something that can be measured fairly easily. This helps the team know what they are aiming for, and also helps the organization know if the application is delivering on its promises.

但这只是对话的开始。接下来,Tracy 和她的团队需要与业务赞助商合作,确定最能实现这一目标的功能目标。

But this is just the start of the conversation. Next, Tracy and her team need to work with the business sponsors to identify the features that will best deliver on this goal.

3.2.2 发现能力和特性

3.2.2 Discovering capabilities and features

在更传统的开发过程中,业务部门会提供一份要实施的需求列表。在一般的敏捷项目中,尤其是对于实践 BDD 的团队来说,团队成员更多地参与需求发现和解决方案寻找过程。Tracy 和她的团队也不例外。

In a more traditional development process, the business hands off a list of requirements to implement. In Agile projects in general, and for teams practicing BDD in particular, team members are much more engaged in the requirements discovery and solution-finding process. Tracy and her team are no exception.

在 BDD 中,我们将这个初始阶段称为推测阶段,因为我们正在推测哪些功能可能有助于组织实现其目标。例如,如果火车晚点,通知乘客的功能将有助于实现总体目标,因为它将使旅行者有机会通过相应地更改计划来节省时间。另一方面,让乘客评价火车站的功能可能不会被认为具有特别高的价值,因为它为迟到的乘客提供了很少的可操作信息。

In BDD we call this initial phase the Speculate phase, because we are speculating about what features might help the organization achieve their goals. For example, a feature that notifies commuters if their train is late would contribute to the overall goal, because it would give travelers the opportunity to save time by changing their plans accordingly. On the other hand, a feature that lets commuters rate railway stations might not be considered of particularly high value, because it provides little actionable information for a commuter running late.

许多团队发现影响图(见图 3.3)是一种有用的练习,可以发现并优先考虑有助于实现业务目标的高级功能和特性。影响图帮助业务人员和技术人员通过四个基本问题讨论业务目标:“我们为什么要这样做(即我们的业务目标是什么)?”“我们需要改变谁的行为(即关键参与者是谁)?”“他们的行为可能会发生怎样的变化(即他们的行为发生哪些变化会帮助我们实现业务目标)?”和“哪些软件特性可能支持这种行为变化?”我们将在下一章中更详细地讨论影响图。

Many teams find Impact Mapping (see figure 3.3) a useful exercise to discover and prioritize high-level capabilities and features that can help deliver a business goal. Impact mapping helps business and technical people discuss business goals through the prism of four fundamental questions: “Why are we doing this (i.e., what are our business goals)?” “Whose behavior do we need to change (i.e., who are the key actors)?” “How might their behavior change (i.e., what changes in their behavior would help us achieve our business goals)?” and “What software features might support this behavior change?” We will look at impact mapping in more detail in the next chapter.

图 3.3 识别高级功能和特性的影响图

Figure 3.3 An Impact Map identifying high-level capabilities and features

Tracy 和她的团队与商界人士举办了一场“影响地图”研讨会。这有助于他们更深入地了解该项目。例如,虽然游客和通勤者都会使用该服务,但绝大多数用户都是通勤者,因此针对这些用户的功能将对他们想要实现的结果产生更大的影响。他们确定了他们希望为通勤者提供的两项关键功能:更轻松地规划日常通勤的能力以及应对意外延误或中断的能力。

Tracy and her team run an Impact Mapping workshop with businesspeople. This helps them get a deeper understanding of the project. For example, although both tourists and commuters will be using the service, the vast majority of users will be commuters, so features for these users will have more influence on the outcomes they are trying to achieve. They identify two key capabilities that they want to give commuters: the ability to plan their daily commute more easily and the ability to cope with unexpected delays or interruptions.

最后,他们就以下可能为通勤者提供这些功能的基本功能达成了一致,您可以在影响图的右侧看到这些功能:

Finally, they agree on the following essential features that might give commuters these capabilities, which you can see to the right of the Impact Map:

  • 显示两站之间的最佳路线

  • Shows the optimal route between two stations

  • 让通勤者记录他们最喜欢的旅程

  • Allows commuters to record their favorite trips

  • 向旅客提供有关服务延误的实时火车时刻表信息

  • Provides real-time train timetable information about service delays to travelers

  • 显示火车位置的实时信息

  • Shows real-time information about the location of their train

  • 如果火车晚点,通知乘客

  • Notifies commuters if their train is late

让我们看看你如何描述其中的一些特征。

Let’s look at how you might describe some of these features.

3.2.3 描述特征

3.2.3 Describing features

一次您对要提供的功能有一个大致的了解,您需要更详细地描述它们。描述需求的方法有很多种。敏捷团队喜欢以足够小的格式编写需求的简短大纲,以便放在索引卡上。1实践 BDD 的团队通常使用以下格式作为指导2

Once you have a general idea of the features you want to deliver, you need to describe them in more detail. There are many ways to describe a requirement. Agile teams like to write a short outline of the requirement in a format that’s small enough to fit on an index card.1 Teams practicing BDD often use the following format as a guideline:2

为了<实现业务目标或交付业务价值>     
作为<利益相关者>                                                  
我想要<能做某事>                                 
In order to <achieve a business goal or deliver business value>    
As a <stakeholder>                                                 
I want <to be able to do something>                                

您想要实现什么业务成果?

What business outcomes are you trying to achieve?

谁需要它?

Who needs it?

为了实现这个结果,你必须做什么?

What must you do to help achieve this outcome?

这里的顺序很重要。当您规划功能和故事时,您的主要目标应该是提供商业价值。首先考虑您打算提供什么商业价值,然后考虑谁需要您提议的功能,最后考虑您认为什么功能将支持这一结果。

The order here is important. When you plan features and stories, your principal aim should be to deliver business value. Start out with what business value you intend to provide, then who needs the feature you’re proposing, and finally what feature you think will support this outcome.

这有助于确保每项功能都积极地为实现业务目标做出贡献,从而降低范围蔓延的风险。它还可以提醒你为什么要首先实现此功能。例如,你可以这样说:

This helps ensure that each feature actively contributes to achieving a business goal, and so reduces the risk of scope creep. It also acts as a healthy reminder of why you’re implementing this feature in the first place. For example, you could say something like this:

为了更有效地规划我的行程                     
作为通勤者                                                  
我想知道两站之间的最佳行程      
In order to plan my trips more effectively                     
As a commuter                                                  
I want to know the optimal itinerary between two stations      

你想要提供什么商业价值?

What business value are you trying to provide?

谁对此功能感兴趣?

Who is interested in this feature?

该功能有什么作用?

What will the feature do?

另一种流行的变体使用“作为...我想要...以便”格式:

Another popular variation uses the “as a ... I want ... so that” format:

作为<利益相关者>                              
我想要<某物>                              
这样<我就能实现一些商业目标>      
As a <stakeholder>                             
I want <something>                             
So that <I can achieve some business goal>     

谁将从此功能中受益? 谁想要它?

Who will benefit from this feature? Who wants it?

该功能有什么作用?

What does the feature do?

利益相关者能从这个功能中获得什么商业价值?

What business value will the stakeholder get out of this feature?

这种变体旨在帮助开发人员了解需求的背景,即谁将使用某个功能以及他们期望该功能为他们做什么。利益相关者是指使用该功能的人,或者对其输出感兴趣的人。业务目标确定了为什么需要此功能以及它应该提供什么价值。前面提到的功能的等价物可能是这样的:

This variation aims to help developers understand the context of the requirement in terms of who will be using a feature and what they expect it to do for them. The stakeholder refers to the person using the feature, or who is interested in its output. The business goal identifies why this feature is needed and what value it’s supposed to provide. The equivalent of the feature mentioned earlier might be something like this:

作为通勤者
我想知道两站之间最佳的交通方式
这样我就能快速到达目的地
As a commuter
I want to know the best way to travel between two stations
So that I can get to my destination quickly

有时提及这将如何改善当前情况是有用的。我们可以使用“而目前”或“与目前不同”等短语来比较和对比我们提出的解决方案与当前的工作方式。例如,

Sometimes it is useful to mention how this will be an improvement on the current situation. We can use phrases like “whereas currently” or “unlike currently” to compare and contrast the solution we propose with the current way of working. For example,

作为通勤者
我希望能够轻松找到两站之间的最佳路线
这样我就能快速到达目的地
不像现在我需要在纸质小册子里查找时间表
As a commuter
I want to be able to easily find the optimal route between two stations
So that I can get to my destination quickly
Unlike currently where I need to look up the timetable in a paper booklet

这些都是方便的惯例,但只要您记得清楚地表达业务利益,就没有必要选择一种格式而不是另一种格式。例如,一些经验丰富的从业者很乐意使用“为了...作为...我想要”格式来描述更高级别的功能,其中重点是系统应该提供的业务价值,但当故事显然是关于在以下情况下向特定用户提供价值时,他们会切换到“作为...我想要...以便...这样”来描述功能中更详细的用户故事特征。

All of these are handy conventions, but there’s no obligation to choose one format over another, as long as you remember to express the business benefits clearly. For example, some experienced practitioners are happy to use the “in order to ... as a ... I want” format for higher-level features, where the emphasis is very much on the business value the system should deliver, but they switch to “as a ... I want ... so ... that” for more detailed User Stories within a feature, when the stories clearly are about delivering value to particular users in the context of that feature.

3.3 举例说明:通过示例探索特征

3.3 Illustrate: Exploring a feature with examples

特蕾西她的团队现在有了一份要交付的功能列表,并且很好地了解了这些功能将如何使组织受益。现在是时候开始更详细地探索它们了。做到这一点的一个好方法是要求提供一些示例。

Tracy and her team now have a list of features to deliver, along with a good understanding of how these features will benefit the organization. Now it’s time to start exploring them in more detail. And a great way to do this is to ask for some examples.

3.3.1 发现特征

3.3.1 Discovering the feature

什么时候当你听到用户要求某个功能时,你通常会立即开始构建需要解决的问题的概念模型。这样做很容易让隐含的和未说明的假设模糊你的理解,导致不准确的心理模型和后续错误的实施。要求利益相关者给你提供他们的意思的具体例子是测试和确认你对问题的理解的好方法。

When you hear a user asking for a feature, you’ll often immediately start to build a conceptual model of the problem you need to solve. In doing so, it’s easy to let implicit and unstated assumptions cloud your understanding, leading to an inaccurate mental model and an incorrect implementation further down the line. Asking the stakeholders to give you concrete examples of what they mean is a great way to test and confirm your understanding of the problem.

因此,在开始开发“显示两个车站之间的最佳路线”功能之前,Tracy 召集了铁路网络领域专家 Jill、团队测试员 Tess 和将实施该功能的开发人员 Dave。对话如下:3

So, before work starts on the “show the optimal route between two stations” feature, Tracy gets Jill, the rail network domain expert; Tess, the team’s tester; and Dave, the developer who will be implementing the feature, together. The conversation goes like this:3

特蕾西:您能给我举一个通勤者在两个车站之间旅行的例子吗?

Tracy:  Can you give me an example of a commuter traveling between two stations?

吉尔:当然可以,那么从埃平去市政厅怎么样?

Jill:     Sure, how about going from Epping to Town Hall?

特蕾西:那会是什么样子的?

Tracy:  And what would that look like?

吉尔:嗯,他们必须乘坐 T9 线。这是一条使用率很高的线路,每小时有 8 到 16 趟列车,具体取决于一天中的什么时间。我们只需要提出该线路的下一趟预定行程。

Jill:     Well, they’d have to take the T9 line. That’s a heavily used line, and there are between 8 and 16 trains per hour, depending on the time of day. We’d just need to propose the next scheduled trips on that line.

戴夫:下一班到达的火车总是最好的吗?

Dave:   Is the next train that arrives always the best train to take?

吉尔:不总是这样;如果下一班火车是直达列车,而接下来的一班是特快列车,那么下一班火车可能会更好。

Jill:     Not always; if the next train is an all-stops train, and the following is an express, then the following one might be better.

苔丝:总会有下一班火车吗?

Tess:     Will there always be a next train?

吉尔:不,它们会在午夜左右停运。如果最后一班火车已经开走,我们需要告诉乘客夜间巴士的选择。

Jill:     No; they stop around midnight. If the last train has already left for the night, we would need to tell the commuter about the night bus options.

戴夫: 有没有哪天火车根本不运行?

Dave:   And are there any days the trains don’t run at all?

吉尔:嗯,我想不会,但是我得向时刻表人员核实一下;在某些线路上可能会发生这种情况。

Jill:     Well, I don’t think so, but I would have to check with the timetable people; it might happen on certain lines.

特蕾西:你能否设计一种出行方式,让通勤者可以选择多条线路?

Tracy:  Could you have a trip where a commuter would have the choice of more than one line?

吉尔:是的,从霍恩斯比到中央车站的通勤者可以乘坐 T9 或 T1。行程时间从大约 37 分钟到 48 分钟不等,这些线路上的火车通常每隔几分钟就会到达一次,所以我们需要向通勤者提供足够的信息,告知这两条线路上火车的出发和到达时间。

Jill:     Yes, a commuter going from Hornsby to Central could take the T9 or the T1. The trip time will vary from about 37 minutes to around 48 minutes, and trains from any of these lines typically arrive every couple of minutes, so we’d need to give commuters enough information on departure and arrival times for the trains on both lines.

即使在这个相对简单的例子中,你也能看到一些微妙之处。这并不总是简单地建议下一班火车;你必须向乘客提供所有预定的即将到达的火车的出发和到达时间的详细信息。

Even in this relatively straightforward example, you can see that there are some subtleties. It’s not always a simple matter of proposing the next train; you have to give the commuter details about departure and arrival times for all the scheduled upcoming trains.

Tracy 使用一种称为“示例映射4”的技术来使发现过程更加直观。她使用彩色便签在墙上记录这些示例和规则:蓝色卡片代表业务规则,绿色卡片代表这些规则的示例和反例。粉色卡片代表他们目前没有答案的问题。板顶部的黄色卡片提醒他们正在讨论哪个功能或用户故事。

Tracy uses a technique called Example Mapping4 to make the discovery process more visual. She notes these examples and rules using colored Post-Its on a wall: Blue cards represent the business rules, and green cards represent examples and counterexamples of these rules. Pink cards represent questions that they don’t have answers for right now. A yellow card at the top of the board reminds them which feature or User Story is being discussed.

图3.4 Tracy使用示例图来记录和组织业务规则和示例。

Figure 3.4 Tracy uses an Example Map to record and organize the business rules and examples.

BDD 团队经常使用这样的促进技术来引导这些对话,让他们保持专注,并记录出现的关键示例。示例映射就是这样一种技术;特征映射是另一种技术。我们将在后面的书。

BDD teams often use facilitation techniques like this to guide these conversations, to keep them focused, and to record the key examples that come up. Example Mapping is one such technique; Feature Mapping is another. We will look at both of these in much more detail later on in the book.

3.3.2 将功能切分为用户故事

3.3.2 Slicing the feature into User Stories

经常这些对话揭示了不确定性或复杂性,这促使我们将功能拆分成更小的块。在 Scrum 中,用户故事需要足够小才能在单个冲刺中交付。如果一个功能需要多个冲刺才能构建,那么最好将其拆分成几个故事,这些故事可以在几个冲刺中逐步交付。这样,团队可以更早、更频繁地获得反馈,从而降低出错的风险。

Often these conversations uncover uncertainty or complexity, which leads us to split features into smaller chunks. In Scrum, a User Story needs to be small enough to deliver in a single sprint. If a feature will take more than one sprint to build, it is good practice to split it into several stories that can be delivered incrementally over several sprints. This way, the team can get feedback earlier and more often, which in turn reduces the risk of error.

在这种情况下,Tracy 和她的团队决定将该功能拆分成多个较小的故事。例如,第一个示例卡“只有一条可能的线路”可以作为用户故事本身来交付。这个简单的案例将使他们能够构建初始应用程序架构,而不会产生太多的领域复杂性。我们可以将这个故事描述如下:

In this case, Tracy and her team decide to split the feature into a number of smaller stories. For instance, the first example card, “The one where there is only one possible line,” could be delivered as a User Story in its own right. This simple case will let them build up the initial application architecture without too much domain complexity. We could describe this story as follows:

直接连接

Direct Connections

作为在同一线路上的两个车站之间往返的通勤者

As a commuter traveling between two stations on the same line

我想知道下一班开往我目的地的火车什么时候出发

I want to know what time the next trains for my destination will leave

这样我就可以减少在车站等待的时间

So that I can spend less time waiting at the station

下一个例子“有其他路线可供选择”也能证明其自身的故事:

The next example, “The one where alternative routes are possible,” would also justify its own story:

替代路线

Alternative Routes

作为在不同线路的两个车站之间往返的通勤者

As a commuter traveling between two stations on different lines

我想知道去往目的地最快的火车什么时候出发

I want to know what time the fastest train to my destination will leave

这样我就可以减少在车站或换乘火车的时间

So that I can spend less time waiting at the station or for connecting trains

团队同意这些故事已经足够了。现在让我们看看他们如何将这些故事和例子转化为可执行文件规格。

The team agrees that these are enough stories to get started with. Now let’s see how they turn these stories and examples into executable specifications.

3.4 制定:从示例到可执行规范

3.4 Formulate: From examples to executable specifications

特蕾西的团队现在有一组他们想要实现的故事和示例。在本节中,我们将了解如何采用这些以业务为中心的示例并以可执行规范的形式重写它们。您将了解如何自动化这些规范,以及如何通过这样做来发现您需要编写哪些代码。您还将看到这些可执行规范如何成为强大的报告和动态文档工具。

Tracy’s team now has a set of stories and examples that they want to implement. In this section, we’ll see how to take these business-focused examples and rewrite them in the form of executable specifications. You’ll see how you can automate these specifications, and how doing so leads to discovering what code you need to write. And you’ll see how these executable specifications can be a powerful reporting and living documentation tool.

我们可以使用 Tracy 和她的团队记录的示例作为更正式的验收标准的基础。简而言之,验收标准是让利益相关者(和 QA)满意的,即应用程序能够完成其应有的功能。

We can use examples like the ones Tracy and her team recorded as the basis for more formal acceptance criteria. In a nutshell, the acceptance criteria are what will satisfy stakeholders (and QA) that the application does what it’s supposed to do.

像与 Jill 的对话(见第 2.3.1 节)这样的对话是加深您对问题空间理解的好方法,但如果您使用稍微更结构化的风格,您可以走得更远。在 BDD 中,通常使用以下符号来表达示例:5

Conversations like the one with Jill (in section 2.3.1) are a great way to build up your understanding of the problem space, but you can take things a lot further if you use a slightly more structured style. In BDD, the following notation is often used to express examples:5

给定<一个上下文>
当<某事发生>
然后<你期待一些结果>
Given <a context>
When <something happens>
Then <you expect some outcome>

这种格式可以帮助您思考用户如何与系统交互以及结果应该是什么。正如您将在下一节中看到的那样,它们也很容易使用 Cucumber 等工具转换为自动验收测试。但由于您可能希望稍后自动化这些测试,因此它们的格式不太灵活。正如您将看到的,GivenWhenThen等词对这些工具具有特殊含义,因此最好将它们视为特殊关键字。

This format helps you think in terms of how users interact with the system and what the outcomes should be. As you’ll see in the next section, they’re also easy to convert into automated acceptance tests using tools like Cucumber. But because you may want to automate these tests later on, their format is a little less flexible. As you’ll see, words like Given, When, and Then have special meanings for these tools, so it’s best to think of them as special keywords.

在 BDD 术语中,我们将这些形式化的示例称为场景让我们看看 Tracy 的团队为“直接连接”的故事想出了哪些场景:

In BDD parlance we call these formalized examples scenarios. Let’s see what scenarios Tracy’s team come up with for the Direct Connections story:

直接连接

Direct Connections

只有一条可能的路线

The one where there is only one possible line

作为在同一线路上的两个车站之间往返的通勤者

As a commuter traveling between two stations on the same line

我想知道下一班开往我目的地的火车什么时候出发

I want to know what time the next trains for my destination will leave

这样我就可以减少在车站等待的时间

So that I can spend less time waiting at the station

Tracy 与 Jill 和 Tess 一起研究了一些具体的例子,并使用“给定...何时...然后”符号来表达这些例子:

Tracy gets together with Jill and Tess to work through some concrete examples and to express these using the Given ... When ... Then notation:

泰丝:我们可以从一个简单的例子开始吗?比如说,旅行者特拉维斯想要进行一次只涉及一条线路的旅行?

Tess:     Can we start with a simple example? Say, Travis the traveler wants to take a trip that only involves a single line?

吉尔:当然可以,他可以乘坐 8:02 的车从霍恩斯比到查茨伍德,这是直达车。

Jill:     Sure; he could take the 8:02 from Hornsby to Chatswood, which is a direct trip.

Tess 对此的描述如下:

Tess writes this up as follows:

场景:显示下一班前往所需目的地的火车
  假设下一班火车于 8:02 从霍恩斯比出发                         
  Travis 想要在 8:00 从 Hornsby 前往 Chatswood 时       
  那么他应该被告知乘坐8:02的火车                       
Scenario: Display the next train going to the requested destination
  Given the next train leaves Hornsby at 8:02                         
  When Travis wants to travel from Hornsby to Chatswood at 8:00       
  Then he should be told to take the 8:02 train                       

这个例子的上下文或背景是什么?

What is the context or background for this example?

你描述的是什么动作?

What action are you describing?

预期结果是什么?

What is the expected outcome?

当 Jill 大声朗读这句话时,她注意到了一个小问题。“这不太对,”她说。“有两条线路从霍恩斯比出发,8:02 从霍恩斯比出发的火车可能开往两个方向。你需要知道 Travis 乘坐哪条线路,开往哪个方向。”

When Jill reads this aloud, she notices a glitch. “That’s not quite right,” she says. “There are two lines that depart from Hornsby, and an 8:02 train from Hornsby could go in one of two directions. You need to know which line Travis is taking, and in which direction.”

Tess 对场景进行了相应的改进:

Tess refines the scenario accordingly:

场景:显示下一班前往所需目的地的火车
  鉴于前往 Chatswood 的 T1 列车于 8:02 从 Hornsby 出发               
  Travis 想要在 8:00 从 Hornsby 前往 Chatswood 时        
  那么他应该被告知乘坐 8:02 的火车                
Scenario: Display the next train going to the requested destination
  Given the T1 train to Chatswood leaves Hornsby at 8:02               
  When Travis wants to travel from Hornsby to Chatswood at 8:00        
  Then he should be told to take the 8:02 train                

现在我们同时包括线路名称和方向。

Now we include both the line name and the direction.

吉尔读了新的方案。她仍然不高兴。“这不太现实。特拉维斯只有两分钟的时间买票并到达正确的站台。他真的需要被告知接下来的几趟火车,而不仅仅是下一趟。”

Jill reads the new scenario. She’s still not happy. “That’s not very realistic. Travis only has two minutes to buy a ticket and get to the right platform. He really needs to be told about the next few trains, not just the next one.”

“只显示接下来的两趟列车是否更安全?” Tess 问道。 Jill 也这么认为,因此 Tess 对场景进行了进一步的改进:

“Would it be safer just to show the next two trains?” asks Tess. Jill thinks so, so Tess refines the scenario a bit more:

场景:显示下一班前往所需目的地的火车
  鉴于前往 Chatswood 的 T1 列车分别于 8:02、8:15、8:21 从 Hornsby 出发     
  Travis 想要在 8:00 从 Hornsby 前往 Chatswood 时        
  然后他应该被告知 8:02、8:15 的火车情况                 
Scenario: Display the next train going to the requested destination
  Given the T1 train to Chatswood leaves Hornsby at 8:02, 8:15, 8:21     
  When Travis wants to travel from Hornsby to Chatswood at 8:00        
  Then he should be told about the trains at: 8:02, 8:15                 

确定几个训练时间,使示例更具代表性

Identifies several train times to make the example more representative

现在我们需要告诉 Travis 有关接下来两列火车的情况。

Now we need to tell Travis about the next two trains.

简单地合作写出此类场景是一种很好的方法,可以提高我们对所解决问题的理解,消除隐藏的假设,并降低风险。许多团队发现,仅凭这些对话就可以显著减少缺陷并提高所交付功能的质量。

The simple act of working together to write out scenarios like these is a great way to improve our understanding of the problem we are solving, flush out hidden assumptions, and reduce risk. Many teams find that these conversations alone significantly reduce defects and improve the quality of the delivered features.

但是 BDD 最强大的一点是,这些示例可以以某种方式变为可执行的,这样它们就可以同时成为自动化验收测试和业务可读规范。在下一节中,我们将看到如何使用 Java、Cucumber、Maven 和Git。

But one of the most powerful things about BDD is the idea that these examples can somehow become executable, so that they become simultaneously automated acceptance tests and business-readable specifications. In the next section, we will see how we can transform these acceptance criteria into executable specifications using Java, Cucumber, Maven, and Git.

3.5 自动化:从可执行规范到自动化测试

3.5 Automate: From executable specifications to automated tests

那里有许多专门的 BDD 工具可用于自动化验收标准。常用的选择包括 Cucumber(适用于 Java、JavaScript、Ruby 和许多其他语言)、SpecFlow(适用于 .NET)和 Behave(适用于 Python)。虽然这些工具并非不可或缺,但它们确实使以类似于上一节中使用的 Given ... When ... Then 表达式的结构化形式表达自动化测试变得更加容易。这使产品所有者和测试人员更容易理解和识别自动化验收标准,从而有助于增强他们对自动化测试和自动化验收测试方法的信心。

There are many specialized BDD tools that you can use to automate your acceptance criteria. Popular choices include tools like Cucumber (for Java, JavaScript, Ruby, and many other languages), SpecFlow (for .NET), and Behave (for Python). While not indispensable, these tools do make it easier to express the automated tests in a structured form similar to the Given ... When ... Then expressions used in the previous section. This makes it easier for product owners and testers to understand and identify the automated acceptance criteria, which in turn can help increase their confidence in the automated tests and in the automated acceptance testing approach in general.

3.5.1 使用 Maven 和 Cucumber 设置项目

3.5.1 Setting up a project with Maven and Cucumber

自始至终在本书的其余部分,我们将使用几种不同的 BDD 工具来说明示例。在本章中,我们将了解如何使用 Cucumber 和 Java 编写可执行规范,6并使用 Maven 构建和运行项目。Maven ( http://maven.apache.org/ ) 是 Java 世界中广泛使用的构建工具。测试报告将使用 Serenity BDD 生成,7 这是一个开源库,可以更轻松地组织和报告 BDD 测试结果。

Throughout the rest of this book, we’ll illustrate examples using several different BDD tools. In this chapter, we’ll see how to write executable specifications using Cucumber and Java,6 and the project will be built and run using Maven. Maven (http://maven.apache.org/) is a widely used build tool in the Java world. The test reports will be generated using Serenity BDD,7 an open source library that makes it easier to organize and report on BDD test results.

本章的源代码可在 GitHub 8和 Manning 网站上找到。我们将介绍整个过程,跟随 Tess 和 Dave 在 Cucumber 中实施自动验收测试,并使用这些测试来推动他们的开发过程。如果您想继续学习,您需要一个安装了以下软件的开发环境:

The source code for this chapter is available on GitHub8 and on the Manning website. We will walk through the full process, following along with Tess and Dave as they implement the automated acceptance tests in Cucumber, and use these tests to drive their development process. If you want to follow along, you’ll need a development environment with the following software installed:

  • Java JDK(示例代码是使用 OpenJava 12.0.2 开发的,但它应该可以在 JDK 1.8 或更高版本上运行良好)

  • A Java JDK (the sample code was developed using OpenJava 12.0.2, but it should work fine with JDK 1.8 or higher)

  • Maven 3.6.x 或更高版本

  • Maven 3.6.x or higher

  • Git(如果您想要查看示例解决方案的实际操作并继续操作,您还需要设置一个 GitHub 帐户)

  • Git (if you want to see the sample solution in action and follow along, you will also need to set up a GitHub account)

Tess 和 Dave 做的第一件事是使用 GitHub 9上的 Serenity Cucumber Starter 模板项目创建一个新的 Maven 项目(见图 3.5)。GitHub 模板项目是一种使用特定库集创建骨架项目的便捷方式。如果您有 GitHub 帐户,则可以单击“使用此模板”按钮,在您自己的帐户中创建一个包含模板项目副本的新存储库。如果您没有 GitHub 帐户,则可以单击“代码”菜单下载包含模板项目源代码的 zip 文件。这个特定的项目模板包含一个骨架项目结构,其中包含最新版本的 Cucumber 和 Serenity BDD。

The first thing Tess and Dave do is create a new Maven project using the Serenity Cucumber Starter template project on GitHub9 (see figure 3.5). GitHub template projects are a convenient way to create skeleton projects using a specific set of libraries. If you have a GitHub account, you can click the Use This Template button to create a new repository in your own account with a copy of the template project. And if you don’t have a GitHub account, you can click the Code menu to download a zip file containing the source code for the template project. This particular project template contains a skeleton project structure with recent versions of Cucumber and Serenity BDD.

图 3.5 GitHub 上的 Serenity Cucumber Starter 模板项目

Figure 3.5 The Serenity Cucumber Starter template project on GitHub

Tess 点击“使用此模板”按钮创建一个新的 GitHub 存储库,她将其命名为 train_timetables(见图 3.6)。这会在她自己的帐户中创建一个新的骨架项目,她可以将其克隆到本地机器上:

Tess clicks the Use This Template button to create a new GitHub repository, which she calls train_timetables (see figure 3.6). This creates a new skeleton project in her own account, that she can clone to her local machine:

C:\Users\tess\projects> git 克隆
> https://github.com/bdd-in-action-second-edition/train-timetables.git
克隆到‘火车时刻表’......
远程:枚举对象:43,完成。
远程:计数对象:100%(43/43),完成。
远程:压缩对象:100% (35/35),完成。
接收对象:32% (14/43)sed 23 (delta 1),pack-reused 
0 个接收对象:30% (13/43)
接收对象:100%(43/43),65.71 KiB | 1.29 MiB/s,完成。
C:\Users\tess\projects>git clone 
> https://github.com/bdd-in-action-second-edition/train-timetables.git
Cloning into 'train-timetables'...
remote: Enumerating objects: 43, done.
remote: Counting objects: 100% (43/43), done.
remote: Compressing objects: 100% (35/35), done.
Receiving objects:  32% (14/43)sed 23 (delta 1), pack-reused 
0 receiving objects:  30% (13/43)
Receiving objects: 100% (43/43), 65.71 KiB | 1.29 MiB/s, done.

图 3.6 从模板项目创建新的存储库

Figure 3.6 Creating a new repository from the Template project

接下来,Tess 将模板项目导入她的 IDE(见图 3.7)。此启动项目包含标准目录结构以及一些示例测试类。最重要的文件和目录包括

Next, Tess imports the template project into her IDE (see figure 3.7). This starter project contains a standard directory structure along with a few sample test classes. The most important files and directories include

  • src —包含应用程序源代码的主目录。

  • src—The main directory containing the application source code.

  • src/main/java — 您的应用程序的源代码放在这里。

  • src/main/java—The source code for your application goes here.

  • src/test/java — 这是测试的 Java 类所在的位置。

  • src/test/java—This is where the Java classes for your tests go.

  • src/test/resources — 这是放置测试套件所需的其他文件(例如 Cucumber 功能文件)的地方。

  • src/test/resources—This is where other files needed for the test suite, such as Cucumber feature files, are placed.

  • pom.xml —Maven 构建脚本。

  • pom.xml—The Maven build script.

  • build.gradle — 等效的 Gradle 构建脚本。

  • build.gradle—The equivalent Gradle build script.

图 3.7 新项目加载到 IntelliJ

Figure 3.7 The new project loaded into IntelliJ

如果你在家里跟着做,请进入火车时刻表文件夹并运行mvn verify命令.这将下载您需要的任何依赖项并运行与项目骨架捆绑在一起的简单功能文件:

If you are following along at home, go into the train-timetables folder and run the mvn verify command. This should download any dependencies you need and run the simple feature file that comes bundled with the project skeleton:

$ cd 火车时刻表
$ mvn 验证
[信息] 正在扫描项目...
...
[信息] -------------------------------------------------------
[信息] 测试
[信息] -------------------------------------------------------
...
[信息] --------------------------------------------------------------------------------
[信息] 建设成功
[信息] --------------------------------------------------------------------------------
[INFO] 总时间:14.320 秒
[INFO] 完成于:2021-10-09T07:54:04+01:00
[信息] --------------------------------------------------------------------------------
$ cd train-timetables
$ mvn verify
[INFO] Scanning for projects...
...
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
...
[INFO] --------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] --------------------------------------------------------------------
[INFO] Total time:  14.320 s
[INFO] Finished at: 2021-10-09T07:54:04+01:00
[INFO] --------------------------------------------------------------------

Tess 需要做的最后一项日常工作是将项目变成她自己的项目。她将入门包重命名为与她的组织和项目更相关的名称,并删除示例代码和示例功能文件。她最终得到的项目如图 3.8 所示。

The last piece of housekeeping work Tess needs to do is to make the project her own. She renames the starter package to something more related to her organization and project and deletes the sample code and sample feature file. The project she ends up with looks like the one in figure 3.8.

图 3.8 定制的项目结构已准备就绪

Figure 3.8 The tailored project structure ready to start work

现在,他们已经有了一个可以运行的项目框架,Tess 和 Dave 开始着手更有趣的工作:以 Cucumber 可以实现的形式实现他们之前发现的场景。执行。

Now that they have a project skeleton up and running, Tess and Dave move on to more interesting work: implementing the scenarios they discovered earlier on in a form that Cucumber can execute.

3.5.2 在 Cucumber 中记录可执行规范

3.5.2 Recording the executable specifications in Cucumber

Cucumber,我们将之前写过的场景记录在称为特征文件的特殊文件中。这些文件具有“.feature”后缀,顾名思义,旨在包含描述特定功能行为的所有场景。Tess 记录了她在上一个问题中与 Jill 定义的场景,如下所示。

In Cucumber, we record scenarios like the ones we wrote earlier in special files called feature files. These files have a “.feature” suffix and are designed to contain, as the name suggests, all of the scenarios that describe the behavior of a particular feature. Tess records the scenario that she defined with Jill in the previous question, as follows.

清单 3.1 用 Cucumber 表达的验收标准

Listing 3.1 An acceptance criteria expressed in Cucumber

功能:显示下一班出发的列车
 
  作为在同一线路上的两个车站之间往返的通勤者
  我想知道下一班开往我目的地的火车什么时候出发
  这样我就可以减少在车站等待的时间
  场景:下一班列车将前往同一线路上的请求目的地
    鉴于前往 Chatswood 的 T1 列车分别于 8:02、8:15、8:21 从 Hornsby 出发
    Travis 想要在 8:00 从 Hornsby 前往 Chatswood 时
    然后他应该被告知火车的运行时间:8:02、8:15
Feature: Show next departing trains
 
  As a commuter traveling between two stations on the same line
  I want to know what time the next trains for my destination will leave
  So that I can spend less time waiting at the station
  Scenario: Next train going to the requested destination on the same line
    Given the T1 train to Chatswood leaves Hornsby at 8:02, 8:15, 8:21
    When Travis wants to travel from Hornsby to Chatswood at 8:00
    Then he should be told about the trains at: 8:02, 8:15

这只不过是我们之前讨论过的示例的结构化版本。粗体字词(Feature、Scenario、Given、When 和 Then)是标记功能文件结构的关键字。其他所有内容都是简单的商业语言。

This is little more than a structured version of the example we discussed earlier. The words in bold (Feature, Scenario, Given, When, and Then) are keywords that mark the structure of the feature file. Everything else is plain business language.

Java 项目的常见惯例是将功能文件放在 src/test/resources/features 目录下。在此目录中,功能文件可以按高级功能或主题分组。例如,随着此项目的进展,团队最终可能会得到以下目录

A common convention for Java projects is to place the feature files under the src/test/resources/features directory. Within this directory, feature files can be grouped by high-level capabilities or themes. For example, as this project progresses, the team might end up with directories such as

  • 行程(行程计算和时间表信息)

  • itineraries (itinerary calculations and timetable information)

  • 通勤者(通勤者的个性化出行数据)

  • commuters (personalized trip data for commuters)

  • 通知(通勤者延迟通知)

  • notifications (delay notifications for commuters)

但现在,一个特征文件就足够了。Tess 在 features 文件夹中创建 itineraries 目录,并添加一个名为 show_next_departing_trains.feature 的文件,她在其中记录了清单 3.1 中显示的特征。

But for now, one feature file is enough. Tess creates the itineraries directory in the features folder and adds a file called show_next_departing_trains.feature, where she records the feature shown in listing 3.1.

目录结构现在如下所示:

The directory structure now looks like this:

|____来源
| |____测试
| | |____资源
| | | |____特点
| | | | |____行程
| | | | ||____显示下一趟出发列车.功能
|____src
| |____test
| | |____resources
| | | |____features
| | | | |____itineraries
| | | | | |____show_next_departing_trains.feature

现在,这算作一个可执行规范。尽管该场景背后没有代码来测试任何东西,但您仍然可以执行它。当您执行它时,测试将失败(因为没有代码来实现它们),但 Serenity BDD 仍将生成一份报告,介绍等待实现的场景。如果您想尝试一下,请进入 train-timetables 目录并运行以下命令:

This now counts as an executable specification. Though there is no code behind the scenario to make it test anything, you can still execute it. When you execute it, the tests will fail (because there is no code to implement them), but Serenity BDD will still generate a report presenting the scenarios that are awaiting implementation. If you want to try this out, go into the train-timetables directory and run the following command:

$ mvn clean 验证
$ mvn clean verify

这将产生一个像图 3.9 中那样的丑陋的错误信息,但它也会在 target/site/serenity 目录中生成一组报告。10

This will produce a big ugly error message like the one in figure 3.9, but it will also generate a set of reports in the target/site/serenity directory.10

图3.9 在没有任何底层测试代码的情况下运行可执行规范将会导致错误。

Figure 3.9 Running the executable specification without any underlying test code will result in error.

如果您打开此目录中的 index.xhtml 文件并单击屏幕底部的测试表中唯一的测试,您应该会看到类似图 3.10 的内容。

If you open the index.xhtml file in this directory and click on the only test in the Test table at the bottom of the screen, you should see something like figure 3.10.

图 3.10 验收测试报告中待处理的 Cucumber 功能

Figure 3.10 The Pending Cucumber feature in the acceptance test reports

此时,场景不再是一个简单的文本文档,而是一个可执行规范。它可以作为自动构建过程的一部分运行,以自动确定某个特定功能是否已完成。

At this point, the scenario is no longer a simple text document; it’s now an executable specification. It can be run as part of the automated build process to automatically determine whether a particular feature has been completed.

这些场景中使用的语言与 Jill 在与团队的对话中使用的术语非常接近。当这些场景出现在测试报告中时,使用这种熟悉的语言可以让测试人员、最终用户和其他非开发人员更容易理解正在测试哪些功能以及如何测试它们已测试。

The language used in these scenarios is very close to the terms that Jill used in the conversations with the team. And when the scenarios appear in the test reports, the use of this familiar language makes it easier for testers, end users, and other nondevelopers to understand what features are being tested and how they’re being tested.

3.5.3 自动化可执行规范

3.5.3 Automating the executable specifications

现在现在是时候将此可执行规范转变为自动化测试了。首先,Tess 会仔细检查骨架项目附带的测试运行器类。它配置为运行 features 目录下的所有功能文件,如下所示:

Now it is time to turn this executable specification into an automated test. First Tess double-checks the test runner class that came with the skeleton project. It is configured to run all of the feature files under the features directory, and looks like this:

@RunWith(CucumberWithSerenity.class)
@CucumberOptions(features =“src/test/resources/features/”,   
                 胶水=“manning.bddinaction”
公共类AcceptanceTestSuite {}
@RunWith(CucumberWithSerenity.class)
@CucumberOptions(features="src/test/resources/features/",   
                 glue="manning.bddinaction"
)
public class AcceptanceTestSuite {}

将来,她可能会为这个跑步者班级添加更多选项,但是现在这样就很好了。

At a future date, she may add some more options to this runner class, but for now it will do just fine.

接下来,她和 Dave 编写了一些测试自动化代码,这些代码将在执行场景时调用。记住(来自清单 3.1),他们的场景如下所示:

Next, she and Dave write some test automation code that will be called whenever their scenario is executed. Remember (from listing 3.1), their scenario looks like this:

场景:下一班列车将前往同一线路上的请求目的地
    鉴于开往中央车站的 T1 列车于 08:02、08:15、08:21 从霍恩斯比出发
    Travis 想要在 08:00 从 Hornsby 前往 Chatswood 时
    然后他应该被告知火车的运行时间:08:02、08:15
Scenario: Next train going to the requested destination on the same line
    Given the T1 train to Central leaves Hornsby at 08:02, 08:15, 08:21
    When Travis wants to travel from Hornsby to Chatswood at 08:00
    Then he should be told about the trains at: 08:02, 08:15

他们需要为 Given、When 和 Then 等步骤编写一个方法。Cucumber 使用特殊注释(名称相当恰当@Given@When, 和@Then) 来知道每个场景步骤要运行哪个方法。这些注释使用一种称为 Cucumber Expressions 的特殊符号来标识 Cucumber 场景中代表测试数据的部分(例如,火车线路和车站以及时间)。例如,第一个 Given 步骤的注释需要传入火车线路、出发站和目的地站以及出发时间。我们经常将这种代码称为粘合代码,因为它将场景步骤中的文本绑定到实际的测试自动化或应用程序代码。

They need to write a method for each one of these Given, When, and Then steps. Cucumber uses special annotations (named rather appropriately @Given, @When, and @Then) to know which method to run for each scenario step. These annotations use a special notation called Cucumber Expressions to identify the bits of the Cucumber scenario that represent test data (e.g., the train line and stations, and the times). For example, the annotation for the first Given step needs to pass in the train line, the departure and destination stations, and the departure time. We often call this code glue code, because it binds the text in the scenario steps to actual test automation or application code.

Tess 和 Dave 为此步骤编写的初始代码如下所示:

The initial code that Tess and Dave write for this step looks something like this:

@Given("前往 {station} 的 {line} 火车于 {times} 离开 {station}")
public void theTrainLeavesAt(String line,
                             字符串到,
                             字符串来自,
                             列表<LocalTime> 出发时间) {}
@Given("the {line} train to {station} leaves {station} at {times}")
public void theTrainLeavesAt(String line,
                             String to,
                             String from,
                             List<LocalTime> departureTimes) {}

Tess 将这些方法放在一个名为DepartingTrainsStepDefinitions,她将其放在测试运行器类正下方的步骤包中,如下所示。

Tess places these methods in a class called DepartingTrainsStepDefinitions, which she places in a steps package right underneath the test runner class, as follows.

清单 3.2 一个基本的 Cucumber 场景实现

Listing 3.2 A basic Cucumber scenario implementation

包 com.bddinaction.traintimetables.stepdefinitions;
 
导入io.cucumber.java.参数类型;
导入 io.cucumber.java.en.Given;
导入io.cucumber.java.en.Then;
导入io.cucumber.java.en.When;
 
公共类DepartingTrainsStepDefinitions {
 
    @Given("开往 {} 的 {} 列车于 {} 出发")    
    public void theTrainLeavesAt(String line,
                                 字符串到,
                                 字符串来自,
                                 字符串出发时间){}
 
    @When("Travis 想要在 {} 从 {} 前往 {}")
    public void travel(String from, String to, String leaveTime) {}
 
    @Then(“应该告诉他有关火车的信息:{}”)
    公共无效应该告诉AboutTheTrainsAt(字符串预期时间){}
}
package com.bddinaction.traintimetables.stepdefinitions;
 
import io.cucumber.java.ParameterType;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
 
public class DepartingTrainsStepDefinitions {
 
    @Given("the {} train to {} leaves {} at {}")    
    public void theTrainLeavesAt(String line,
                                 String to,
                                 String from,
                                 String departureTimes) {}
 
    @When("Travis want to travel from {} to {} at {}")
    public void travel(String from, String to, String departureTime) {}
 
    @Then("he should be told about the trains at: {}")
    public void shouldBeToldAboutTheTrainsAt(String expectedTimes) {}
}

对于实践 BDD 的团队来说,这样的代码是通往生产代码的大门。它准确地告诉你底层代码需要做什么才能满足业务需求。从这里开始,我们不仅可以开始思考我们的生产代码应该做什么,还可以开始思考如何最好地测试它。

For teams practicing BDD, code like this is the gateway to production code. It tells you precisely what your underlying code needs to do to satisfy the business requirements. From here, we can start to think not only about what our production code should do, but also how best to test it.

这就是 Tess 和 Dave 接下来要做的事情。让我们来跟踪他们的进展,看看他们如何将这些空方法转变为成熟的自动化验收测试,然后使用这些测试来推动生产代码。

And this is what Tess and Dave do next. Let’s follow their progress, as they turn these empty methods into fully fledged automated acceptance tests, and then use these tests to drive out the production code.

3.5.4 实现粘合代码

3.5.4 Implementing the glue code

身体缺陷诊断从业者喜欢从他们需要获得的结果开始,然后倒推,因此 Tess 和 Dave 从@Then步骤开始,该步骤表达了他们期望的结果。他们设想了一个服务来实现时间表逻辑。他们还不太确定这个服务应该是什么样子,但他们知道它需要为他们提供一份拟议的出发时间列表。编写粘合代码为他们提供了绝佳的机会来尝试不同的 API 设计,看看他们最喜欢哪种。

BDD practitioners like to start with the outcome they need to obtain and work backward, so Tess and Dave start with the @Then step, which expresses the outcome they expect. They imagine a service to implement the timetable logic. They aren’t too sure what this service should look like just yet, but they do know that it needs to give them a list of proposed departure times. And writing the glue code gives them the perfect opportunity to experiment with different API designs and see what they like best.

在 @Then 方法中定义期望

Defining the expectations in the @Then method

苔丝首先更新@Then方法来描述她期望的结果:

Tess starts by updating the @Then method to describe the outcome she expects:

  @Then(“应该在以下时间告诉他火车的情况:{times}”)                    
  public void shouldBeToldAboutTheTrainsAt(List<LocalTime> expected) {
    断言(proposedDepartures).isEqualTo(预期);
  }
 
  @ParameterType(“.*”)                                                       
  公共 LocalTime 时间(字符串时间值) {
    返回 LocalTime.parse(timeValue, DateTimeFormatter.ofPattern("H:mm"));
  }
 
  @ParameterType(“.*”)
  公共列表<LocalTime> times(String timeValue) {                           
    返回流(timeValue.split(“,”))
            .map(字符串::trim)
            .map(这个::时间)
            .收集(收集器.toList());
  }
  @Then("he should be told about the trains at: {times}")                   
  public void shouldBeToldAboutTheTrainsAt(List<LocalTime> expected) {
    assertThat(proposedDepartures).isEqualTo(expected);
  }
 
  @ParameterType(".*")                                                      
  public LocalTime time(String timeValue) {
    return LocalTime.parse(timeValue, DateTimeFormatter.ofPattern("H:mm"));
  }
 
  @ParameterType(".*")
  public List<LocalTime> times(String timeValue) {                          
    return stream(timeValue.split(","))
            .map(String::trim)
            .map(this::time)
            .collect(Collectors.toList());
  }

我们引入了一个名为 {times} 的自定义 Cucumber 表达式来表示预期时间列表。

We introduce a custom Cucumber Expression called {times} to represent a list of expected times.

我们定义 {time} 和 {times} Cucumber 表达式来解析字符串值并分别将它们转换为单个日期或日期列表。

We define the {time} and {times} Cucumber Expressions to parse string values and convert them to either a single date or a list of dates, respectively.

我们定义 {time} 和 {times} Cucumber 表达式来解析字符串值并分别将它们转换为单个日期或日期列表。

We define the {time} and {times} Cucumber Expressions to parse string values and convert them to either a single date or a list of dates, respectively.

当然,这段代码无法编译。他们需要声明proposedDepartures变量并确定拟建的火车从哪里来。但这需要时间。

Naturally, this code doesn’t compile. They need to declare the proposedDepartures variable and figure out where the proposed train will come from. But this will come in good time.

在编写代码之前编写可执行规范是发现和充实实现业务目标所需的技术设计的一种很好的方法。它可以帮助您发现哪些域类有意义、您可能需要哪些服务以及服务需要如何相互交互。它还可以帮助您思考如何让您的代码易于测试。易于测试的代码也易于维持。

Writing the executable specifications before writing the code is a great way to discover and flesh out the technical design you need in order to deliver the business goals. It helps you discover what domain classes would make sense, what services you might need, and how the services need to interact with each other. It also helps you think about how to make your code easy to test. And code that is easy to test is easy to maintain.

在@When方法中发现服务类API

Discovering the service class API in the @When method

在这种情况下,Tess 设想了一种简单的方法,称为findNextDepartures()查找从给定车站出发的下一班列车的时间。当她将此代码添加到步骤定义方法中时,结果如下所示:

In this case, Tess imagines a simple method called findNextDepartures() to find the next departure times from a given station. When she adds this code to the step definition method, the result looks something like this:

  列表 <LocalTime> 提议的出发时间;
 
  @When("Travis 想要在 {time} 从 {} 前往 {}")                     
  public void travel(String from, String to, LocalTime 出发时间) {
    建议出发 = itineraryService.findNextDepartures(出发时间,
                                                             从,到);      
  }
 
  @ParameterType(“.*”)                                                       
  公共 LocalTime 时间(字符串时间值) {
    返回 LocalTime.parse(timeValue);
  }
  List<LocalTime> proposedDepartures;
 
  @When("Travis want to travel from {} to {} at {time}")                    
  public void travel(String from, String to, LocalTime departureTime) {
    proposedDepartures = itineraryService.findNextDepartures(departureTime, 
                                                             from, to);     
  }
 
  @ParameterType(".*")                                                      
  public LocalTime time(String timeValue) {
    return LocalTime.parse(timeValue);
  }

在 When 步骤中,我们添加 {time} 表达式来描述预计出发时间。

In the When step we add a {time} expression to describe the expected departure time.

查找下一班出发时间

Finding the next departure times

我们的 {time} Cucumber 表达式将字符串参数转换为 Java 时间对象。

Our {time} Cucumber Expression converts a string parameter into a Java Time object.

“但是它从哪里来呢itineraryService?”Dave 想知道。“我们需要提前定义它。”他在类的顶部添加了以下行来创建服务:

“But where does the itineraryService come from?” Dave wonders. “We’ll need to define it earlier on.” He adds the following line at the top of the class to create the service:

    行程服务 itineraryService = new ItineraryService();
    ItineraryService itineraryService = new ItineraryService();

接下来,他创建了ItineraryService班级以及该findNextDepartures()方法的空实现

Next, he creates the ItineraryService class itself, along with an empty implementation of the findNextDepartures() method:

  公共类行程服务{
      公共列表<LocalTime> findNextDepartures(LocalTime 出发时间,                      
                                                字符串来自,字符串到){
        返回空值;
    }
  }
  public class ItineraryService {
      public List<LocalTime> findNextDepartures(LocalTime departureTime,                      
                                                String from, String to) {
        return null;
    }
  }

“他们itineraryService还需要了解我们在给定步骤中提到的时间表细节,”Tess 指出。“这是一个完全不同的问题,所以它应该放在自己的类中。”

“The itineraryService will also need to know about the timetable details we mention in the Given step,” points out Tess. “That’s a pretty separate concern, so it probably should go in its own class.”

“我同意,”戴夫说。“但我们先不要偏离主题;时间表的逻辑可能相当复杂。我们先创建一个TimeTable界面模拟我们的行程服务如何与时间表服务互动。”

“I agree,” says Dave. “But let’s not get sidetracked by that just yet; the timetable logic could be pretty hairy. Let’s just create a TimeTable interface to model how our itinerary service needs to interact with the timetable service.”

这是一种典型的 BDD 方法,通常称为由外而内。当我们实现一个层时,我们会发现它需要运行的其他东西,它需要调用的其他服务。我们有一个选择:我们可以立即构建这些东西,也可以将它们放在一边,将它们建模为接口或虚拟类,稍后再回来。如果第一种方法适用于较简单的问题,那么对于更复杂的代码,专注于手头的工作通常要高效得多。

This is a typical BDD approach, often referred to as outside-in. As we implement a layer, we will discover other things it needs to function, other services it needs to call. We have a choice: we can either build these things straight away, or we can put them to one side, model them as an interface or a dummy class, and come back to them later. If the first approach works for simpler problems, for more complex code it is generally much more efficient to stay focused on the work at hand.

为了通过这个验收标准,我们的英雄现在需要实现这个findNextDepartures()方法。但要使这种情况奏效,他们需要改变策略,从验收测试转向单元测试。正如您所看到的,验收测试用于演示应用程序的高级端到端行为,而单元测试用于构建实现此行为的组件行为。

To get this acceptance criteria to pass, our heroes now need to implement the findNextDepartures() method. But to make this scenario work, they need to change gears, and go from acceptance testing to unit testing. As you’ll see, acceptance testing is used to demonstrate the high-level, end-to-end behavior of an application, and unit testing is used to build up the components that implement this behavior.

从验收测试到单元测试

going from acceptance tests to unit tests

验收测试通常使用完整或接近完整的应用程序堆栈,而单元测试则专注于单独的组件。单元测试使人们更容易专注于让特定类工作并确定它需要哪些其他服务或组件。单元测试还使检测和隔离错误或回归变得更容易。您通常会编写许多小型单元测试才能通过验收标准(见图 3.11)。在单元测试级别,实践 BDD 的团队经常使用 TDD 来推动实际实施。

Acceptance tests often use a full or near-full application stack, whereas unit tests concentrate on individual components in isolation. Unit tests make it easier to focus on getting a particular class working and identifying what other services or components it needs. Unit tests also make it easier to detect and isolate errors or regressions. You’ll typically write many small unit tests in order to get an acceptance criterion to pass (see figure 3.11). At the unit testing level, teams practicing BDD often use TDD to drive out the actual implementation.

图 3.11 您通常需要编写许多低级、TDD 风格的单元测试才能通过自动验收标准。

Figure 3.11 You’ll typically need to write many low-level, TDD-style unit tests to get an automated acceptance criterion to pass.

正如我们在上一章中看到的,TDD 看似简单。你编写一个测试来描述你期望应用程序如何运行。当然,它会失败,因为你没有编写任何代码。现在,你只需编写足够的代码来通过这个测试。一旦测试通过,你就会查看代码并思考如何整理它、重构它以改进设计,或者只是让它在你以后回头看时更容易阅读和理解。

As we saw in the previous chapter, TDD is deceptively simple. You write a test that describes how you expect your application to behave. Naturally, it will fail, because you haven’t written any code. Now, you write just enough code to make this test pass. And once your test passes, you look at your code and think about how you could tidy it up, refactor it to improve the design, or simply make it easier to read and understand when you come back to it down the track.

BDD 和 TDD 都是示例驱动开发的例子。在这两种方法中,我们都使用具体的例子来说明、讨论和理解我们想要实现的行为。主要区别在于 TDD 倾向于以开发人员为中心的活动,并在类、方法和 API 的详细级别上运行。正如我们所见,BDD 以团队为中心,着眼于业务目标、功能和场景。

Both BDD and TDD are examples of example-driven development. In both approaches, we use concrete examples to illustrate, discuss, and understand the behavior we want to implement. The main difference is that TDD tends to be a developer-centric activity and operates at the detailed level of classes, methods, and APIs. BDD, as we have seen, is team centric and looks at the bigger picture of business goals, features, and scenarios.

编写简单的 TDD 测试用例

Writing a simple TDD test case

让我们回到 Tess 和 Dave。他们将使用 JUnit 5(Java 的一个通用单元测试库)编写单元测试。我们将在本书后面介绍一些更多特定于 BDD 的单元测试工具。正如我们将看到的,现代单元测试库通常包含对 BDD 样式测试的良好支持。Tess 和 Dave 从一个单元测试开始,该测试说明了该findNextDepartures()方法的一个非常基本的用例,如下所示。

Let’s get back to Tess and Dave. They will be writing their unit tests using JUnit 5, a common unit testing library for Java. We’ll look at some more BDD-specific unit testing tools later on in the book. As we will see, modern unit testing libraries often include good support for BDD-style testing. Tess and Dave start with a unit test that illustrates a very basic use case of the findNextDepartures() method, as follows.

清单 3.3 一个简单的 BDD 风格单元测试

Listing 3.3 A simple BDD-style unit test

包 manning.bddinaction.itineraries;
 
导入 org.junit.jupiter.api.DisplayName;
导入 org.junit.jupiter.api.Test;
导入java.time.LocalTime;
导入java.util.List;
 
导入静态 org.assertj.core.api.Assertions.assertThat;
 
@DisplayName("当查找下一班火车出发时间时")
类 WhenFindingNextDepatureTimes {
 
    @测试
    @DisplayName(“我们应该在要求的时间之后搭乘第一班火车”)
    void tripWithOneScheduledTime() {
  
        // 给定
        ItineraryService itineraryService = new ItineraryService();       
 
        // 什么时候
        列表<LocalTime>proposedDepartures
            = itineraryService.findNextDepartures(LocalTime.of(8:25),     
                                                  “霍恩斯比”,              
                                                  “中央”);             
 
        // 然后
        断言(proposedDepartures)                                    
            .containsExactly(LocalTime.of(8:30));                         
    }
}
package manning.bddinaction.itineraries;
 
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.LocalTime;
import java.util.List;
 
import static org.assertj.core.api.Assertions.assertThat;
 
@DisplayName("When finding the next train departure times")
class WhenFindingNextDepatureTimes {
 
    @Test
    @DisplayName("we should get the first train after the requested time")
    void tripWithOneScheduledTime() {
  
        // Given
        ItineraryService itineraryService = new ItineraryService();      
 
        // When
        List<LocalTime> proposedDepartures
            = itineraryService.findNextDepartures(LocalTime.of(8:25),    
                                                  "Hornsby",             
                                                  "Central");            
 
        // Then
        assertThat(proposedDepartures)                                   
            .containsExactly(LocalTime.of(8:30));                        
    }
}

创建新的行程服务。

Create a new itinerary service.

查找 8:25 之后从 Hornsby 到 Central 的出发时间。

Look up the departure times from Hornsby to Central after 8:25.

检查服务是否在预计时间 8:30 返回。

Check that the service returns at the expected time of 8:30.

“我对这个测试不太满意,”Tess 说。“我知道我们想找到 8:25 之后从霍恩斯比开往中央车站的下一班火车。但答案为什么是 8:30 却没有说明清楚。我们依赖的测试数据可能会发生变化吗?这个时间从何而来?”

“I’m not too happy with this test,” says Tess. “I can see that we want to find the next train from Hornsby to Central after 8:25. It doesn’t make it very clear why the answer is 8:30. Are we relying on test data that might change? Where does this time come from?”

“这是一个很好的观点,”戴夫说。“我们需要制定一个时间表并设置一些测试数据。我们不知道TimeTableAPI到底是什么应该看起来像这样,但我们可以将其包装在一个方法中,该方法会创建一个时间表,该时间表将始终返回特定的出发时间列表。” Dave 将单元测试重构为如下所示:

“That’s a good point,” says Dave. “We need to include a timetable and set up some test data. We don’t know exactly what the TimeTable API should look like, but we could wrap this in a method that creates a timetable that will always return a certain list of departure times.” Dave refactors the unit test to look like this:

    私有 LocalTime at(字符串时间){
    返回 LocalTime.parse(time, DateTimeFormatter.ofPattern("H:mm"));     
    }
 
    私人时间表出发(LocalTime ...出发){                
         返回空值; 
    }                          
 
    @测试
@DisplayName("应为出发时间之后的第一个时间")
void tripWithOneScheduledTime() {
 
    // 给定
    timeTable = 出发时间(在("8:10"), 在("8:20"), 在("8:30"));            
    行程 = 新 ItineraryService(时间表);                         
 
    // 什么时候
    列表<LocalTime>proposedDepartures
       = itineraries.findNextDepartures(at("8:25"),"霍恩斯比","中央");   
 
    // 然后
    断言(proposedDepartures).containsExactly(at("8:30"));            
}
    private LocalTime at(String time) {
    return LocalTime.parse(time, DateTimeFormatter.ofPattern("H:mm"));    
    }
 
    private TimeTable departures(LocalTime... departures) {               
         return null; 
    }                          
 
    @Test
 @DisplayName("should the first after the departure time")
 void tripWithOneScheduledTime() {
 
    // Given
    timeTable = departures(at("8:10"), at("8:20"), at("8:30"));           
    itineraries = new ItineraryService(timeTable);                        
 
    // When
    List<LocalTime> proposedDepartures
       = itineraries.findNextDepartures(at("8:25"),"Hornsby","Central");  
 
    // Then
    assertThat(proposedDepartures).containsExactly(at("8:30"));           
 }

创建 LocalDate 的实用方法

A utility method to create a LocalDate

一旦我们知道 TimeTable 类如何工作,此方法将返回正确配置的 TimeTable。

This method will return a properly configured TimeTable, once we know how the TimeTable class works.

创建一个包含指定时间出发的火车时刻表。

Create a timetable with trains that depart at specified times.

创建使用此时间表的行程服务。

Create an itinerary service that uses this timetable.

查找 8:25 之后从 Hornsby 到 Central 的出发时间。

Look up the departure times from Hornsby to Central after 8:25.

检查服务是否在预计时间 8:30 返回。

Check that the service returns at the expected time of 8:30.

“我们可以猜测TimeTable需要,但可能更容易开始实施该findNextDepartures()方法看看我们需要时间表提供什么信息,”苔丝建议道。

“We could guess at what methods the TimeTable class needs, but it might be easier just to start to implement the findNextDepartures() method and see what information we need the timetable to provide,” suggests Tess.

经过一些实验后,Tess 和 Dave 一致认为,这种方法的主要工作是找出哪些线路在两个车站之间经过(这是时刻表应该知道的事情),并找出在指定时间之后到达的下两列火车,如下所示。

After some experimenting, Tess and Dave agree that the main job of this method is to find out which lines go between the two stations (this is something the timetable should know) and to find the next two trains to arrive after the specified time, as follows.

清单 3.4ItineraryService

Listing 3.4 The ItineraryService class

包 manning.bddinaction.itineraries;
 
导入 manning.bddinaction.timetables.TimeTable;
 
导入java.time.LocalTime;
导入java.util.List;
导入java.util.stream.Collectors;
 
公共类行程服务{
    私人时间表时间表;
 
    公共行程服务(时间表时间表){
        这个.时间表 = 时间表;
    }
 
    公共列表<LocalTime> findNextDepartures(LocalTime 出发时间, 
                                              字符串来自, 
                                              字符串到){
 
        var lines = timeTable.findLinesThrough(from, to);                 
 
        返回 lines.stream()
               .flatMap(line -> timeTable.getDepartures(line)             
                                         .stream()                       
               .filter(trainTime -> !trainTime.isBefore(departureTime))   
               .sorted()                                                  
               .limit(2)                                                  
               .collect(Collectors.toList())                             
    }
}
package manning.bddinaction.itineraries;
 
import manning.bddinaction.timetables.TimeTable;
 
import java.time.LocalTime;
import java.util.List;
import java.util.stream.Collectors;
 
public class ItineraryService {
    private TimeTable timeTable;
 
    public ItineraryService(TimeTable timeTable) {
        this.timeTable = timeTable;
    }
 
    public List<LocalTime> findNextDepartures(LocalTime departureTime, 
                                              String from, 
                                              String to) {
 
        var lines = timeTable.findLinesThrough(from, to);                
 
        return lines.stream()
               .flatMap(line -> timeTable.getDepartures(line)            
                                         .stream())                      
               .filter(trainTime -> !trainTime.isBefore(departureTime))  
               .sorted()                                                 
               .limit(2)                                                 
               .collect(Collectors.toList());                            
    }
}

询问经过出发站和目的地站的线路时刻表

Asks the timetable for the lines going through the departure and destination stations

向时刻表询问这些线路的出发时间列表

Asks the timetable for the list of departure times on these lines

仅保留不早于请求出发时间的出发时间

Keeps only the departure times that are not before the requested departure time

首先显示较早的列车

Shows the earlier trains first

仅保留前两个出发时间

Only keeps the first two departure times

以 LocalTime 对象列表的形式返回这些内容

Returns these as a list of LocalTime objects

最后,他们在测试中实现了时间表的虚拟版本,它只返回一个硬编码的时间列表。该类的实际TimeTable逻辑最终会比这复杂得多,但从行程服务的角度来看,它需要知道的是时间表会在它要求时发回正确的出发时间。

Finally, they implement a dummy version of the timetable in their test, one that just returns a hard-coded list of times. The actual logic of the TimeTable class will eventually be much more complex than this, but from the point of view of the itinerary service, all it needs to know is that the timetable will send back the right departure times when it asks for them.

这是 BDD 团队普遍采用的由外而内的开发风格的一个很好的例子。在编写这个类时,Tess 和 Dave 发现了他们需要从这个TimeTable类中获得的两件事:它需要告诉他们哪条火车线路经过任意两个车站,以及每条线路上的火车什么时候离开给定车站。他们已经精确地确定了他们需要的方法以及这些方法应该做什么。

This is a good example of the outside-in development style commonly practiced in BDD teams. In writing this class, Tess and Dave have discovered two things they need from the TimeTable class: it needs to tell them which train lines go through any two stations and also what time trains leave a given station on each line. They have precisely identified the methods they need and what these methods should do.

基于此实现,TimeTable接口至少需要包含这些方法:

Based on this implementation, the TimeTable interface needs to include at least these methods:

公共接口时间表{
    列表 <String> findLinesThrough(字符串来自,字符串到);
    List<LocalTime> getDepartures(String lineName, String from);
}
public interface TimeTable {
    List<String> findLinesThrough(String from, String to);
    List<LocalTime> getDepartures(String lineName, String from);
}

后续可能会有更多,但是从行程服务的角度来看,这些已经足够了。

It might have more later on, but from the point of view of the itinerary service, these will be enough.

现在他们已经定义了TimeTable接口,他们可以返回到原来的测试并完成该departures()方法为了返回出发时间,我们要求它:

Now that they have defined the TimeTable interface, they can return to the original test and complete the departures() method so that it returns the departure times we ask it to:

私人时间表出发(本地时间......出发){
    返回新的时间表(){
      @Override
      公共列表<String> findLinesThrough(字符串来自,
                                           字符串到){
        返回 List.of("T1");
      }
 
      @Override
      公共列表<LocalTime> getDepartures(字符串线,字符串从){
        返回列表(出发);
      }
    };
    }
private TimeTable departures(LocalTime... departures) {
    return new TimeTable() {
      @Override
      public List<String> findLinesThrough(String from,
                                           String to) {
        return List.of("T1");
      }
 
      @Override
      public List<LocalTime> getDepartures(String line, String from) {
        return List.of(departures);
      }
    };
    } 

成功了!通过这个虚拟实现,他们的第一个单元测试通过了。“太棒了!”Tess 说。“这个类还需要做什么?”

Success! With this dummy implementation, their first unit test passes. “Great!” says Tess. “What else does this class need to do?”

两人继续探索行程服务的行为,并添加一些测试来说明此行为的不同方面。例如,他们想添加返回多个预定时间的场景,以确保只返回前两个,并且他们想检查没有更多预定时间的极端情况火车,如下面的清单所示。

The pair continue to explore the behavior of the itinerary service and add a few more tests to illustrate different facets of this behavior. For example, they want to add scenarios where multiple scheduled times are returned to make sure that only the first two are returned, and they want to check the edge case where there are no more trains, as in the following listing.

清单 3.5 完成的WhenFindingNextDepatureTimes

Listing 3.5 The completed WhenFindingNextDepatureTimes class

包 manning.bddinaction.itineraries;
 
导入 manning.bddinaction.timetables.TimeTable;
导入 org.junit.jupiter.api.DisplayName;
导入 org.junit.jupiter.api.Test;
导入java.time.LocalTime;
导入java.time.format.DateTimeFormatter;
导入java.util.List;
导入静态 org.assertj.core.api.Assertions.assertThat;
 
@DisplayName("当查找下一班出发时间时")
类 WhenFindingNextDepatureTimes {
 
    私有 LocalTime at(字符串时间){
        返回 LocalTime.parse(time, DateTimeFormatter.ofPattern("H:mm"));
    }
 
    私有静态时间表出发(LocalTime ... 出发){        
        返回新的时间表(){
 
            @Override
            公共列表<String> findLinesThrough(String departingFrom,  
                                                 字符串 goingTo) {
                返回 List.of("T1");
            }
 
            @Override
            公共列表<LocalTime> getDepartures(字符串行,
                                                 字符串来自){
                返回列表(出发);
            }
        };
    }
 
    时间表时间表;
    行程服务行程;
 
    @测试
    @DisplayName("应为出发时间之后的第一个时间")
    void tripWithOneScheduledTime() {
 
        timeTable = 出发时间(在("8:10"), 在("8:20"), 在("8:30"));       
        行程 = 新的 ItineraryService(时间表);
 
        列表<LocalTime>proposedDepartures
            = itineraries.findNextDepartures(at("8:25"),                  
                                             “霍恩斯比”,“中央”);        
 
        断言(proposedDepartures).containsExactly(at("8:30"));       
 
    }
 
    @测试
    @DisplayName(“应提议接下来的两趟列车”)    
    void tripWithSeveralScheduledTimes() {                                
 
        时间表 
          = 出发(于("8:10"), 于("8:20"), 于("8:30"), 于("8:45"));
        行程 = 新的 ItineraryService(时间表);
        列表<LocalTime>proposedDepartures
         = itineraries.findNextDepartures(at("8:05"),"霍恩斯比","中央");
 
        断言(提议出发)
          .包含精确(在(“8:10”),在(“8:20”));
    }
 
    @测试
    @DisplayName("如果没有可用的火车,则不应返回")   。❻
    无效的后小时旅行(){
 
        时间表 = 出发时间(“8:10”), (“8:20”), (“8:30”));
        行程 = 新的 ItineraryService(时间表);
 
        列表<LocalTime>proposedDepartures
                = itineraries.findNextDepartures(at("8:50"),
                霍恩斯比、中央);
 
        断言(proposedDepartures).isEmpty();
    }
}
package manning.bddinaction.itineraries;
 
import manning.bddinaction.timetables.TimeTable;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
 
@DisplayName("When finding the next departure times")
class WhenFindingNextDepatureTimes {
 
    private LocalTime at(String time) {
        return LocalTime.parse(time, DateTimeFormatter.ofPattern("H:mm"));
    }
 
    private static TimeTable departures(LocalTime... departures) {       
        return new TimeTable() {
 
            @Override
            public List<String> findLinesThrough(String departingFrom,  
                                                 String goingTo) {
                return List.of("T1");
            }
 
            @Override
            public List<LocalTime> getDepartures(String line,
                                                 String from) {
                return List.of(departures);
            }
        };
    }
 
    TimeTable timeTable;
    ItineraryService itineraries;
 
    @Test
    @DisplayName("should the first after the departure time")
    void tripWithOneScheduledTime() {
 
        timeTable = departures(at("8:10"), at("8:20"), at("8:30"));      
        itineraries = new ItineraryService(timeTable);
 
        List<LocalTime> proposedDepartures
            = itineraries.findNextDepartures(at("8:25"),                 
                                             "Hornsby","Central");       
 
        assertThat(proposedDepartures).containsExactly(at("8:30"));      
 
    }
 
    @Test
    @DisplayName("should propose the next 2 trains")    
    void tripWithSeveralScheduledTimes() {                               
 
        timeTable 
          = departures(at("8:10"), at("8:20"), at("8:30"), at("8:45"));
        itineraries = new ItineraryService(timeTable);
        List<LocalTime> proposedDepartures
         = itineraries.findNextDepartures(at("8:05"),"Hornsby","Central");
 
        assertThat(proposedDepartures)
          .containsExactly(at("8:10"), at("8:20"));
    }
 
    @Test
    @DisplayName("No trains should be returned if none are available").  
    void anAfterHoursTrip() {
 
        timeTable = departures(at("8:10"), at("8:20"), at("8:30"));
        itineraries = new ItineraryService(timeTable);
 
        List<LocalTime> proposedDepartures
                = itineraries.findNextDepartures(at("8:50"),
                "Hornsby", "Central");
 
        assertThat(proposedDepartures).isEmpty();
    }
}

创建虚拟时间表以用于测试

Creates a dummy timetable for testing purposes

伪造的时间表,返回一组硬编码的出发时间

A fake timetable that returns a hard-coded set of departure times

调用 ItineraryService 来查找下一班航班的出发时间

Calls the ItineraryService to find the next departure times

查看预计出发时间

Checks with the expected departure times

一个稍微复杂一点的测试,检查我们返回的次数不超过两次

A slightly more sophisticated test that checks that we return no more than two times

进行边缘情况测试,检查上次出发时间后是否没有返回任何时间

An edge-case test to check that no times are returned after the last departure time

在 @Given 方法中自动化先决条件

Automating the preconditions in the @Given method

行程服务完成后,时间步骤即可运行。现在是时候继续进行给定步骤了:

With the itinerary service complete, the When step is now operational. It is time to move on to the Given step:

鉴于开往中央车站的 T1 列车于 8:02、8:15、8:21 从霍恩斯比出发    
Given the T1 train to Central leaves Hornsby at 8:02, 8:15, 8:21    

此步骤需要准备TimeTable行程服务将使用的。虽然他们可以使用虚拟时间表(如我们在上一个单元测试中看到的时间表),但像这样的 BDD 场景通常希望验证所有系统组件是否按应有的方式协同工作。

This step needs to prepare the TimeTable that the itinerary service will use. While they could use a dummy timetable, like the one we saw in the previous unit test, BDD scenarios like this generally want to verify that all the system components work together as they should.

“看来我们需要在这一步准备实际的时间表数据。那会是什么样子的?” Tess 想知道。

“It looks like we need to prepare the actual timetable data in this step. What would that look like?” wonders Tess.

CanScheduleServices“我们创建一个界面怎么样?”使用scheduleService方法来表示这种能力?我们还可以将其添加到TimeTable界面中但这感觉像是另一个问题,”戴夫说。“就像这”:

“How about we create a CanScheduleServices interface with a scheduleService method to represent this ability? We could add also add it to the TimeTable interface, but it feels like a separate concern,” suggests Dave. “Something like this”:

公共接口 CanScheduleServices{
    void scheduleService(字符串行,
                         列表<LocalTime> 出发时间,
                         弦乐的离去,
                         字符串目的地);
}
public interface CanScheduleServices{
    void scheduleService(String line,
                         List<LocalTime> departingAt,
                         String departure,
                         String destination);
}

Tess 重构了 Given 步骤的粘合代码以使用此方法:

Tess refactors the glue code for the Given step to use this method:

    InMemoryTimeTable timeTable = new InMemoryTimeTable();
    行程服务 itineraryService = 新的行程服务 (时间表);
 
    @Given("前往 {} 的 {} 列车于 {times} 出发 {}")
    public void theTrainLeavesAt(String line,
                                 字符串来自,
                                 字符串到,
                             列表<LocalTime> 出发时间) {
        列表 <LocalTime> 出发时间 = localTimesFrom(出发时间);
        timeTable.scheduleService(线路,出发时间,从,到);       
    }
    InMemoryTimeTable timeTable = new InMemoryTimeTable();
    ItineraryService itineraryService = new ItineraryService(timeTable);
 
    @Given("the {} train to {}  leaves {}  at {times}")
    public void theTrainLeavesAt(String line,
                                 String from,
                                 String to,
                             List<LocalTime> departureTimes) {
        List<LocalTime> departureTimes = localTimesFrom(departingAt);
        timeTable.scheduleService(line, departureTimes, from, to);      
    }

安排特定线路的出发时间

Schedules the departure time for a specific line

实现服务

Implementing the service

“现在我们只需要编写一个实现这两个TimeTable接口的类CanScheduleService界面” Tess 说。两人再次使用测试优先的策略来设想和实现一个TimeTable。他们决定从一个名为 的简单实现开始InMemoryTimeTable。他们从一个空的实现开始,如下所示:

“Now we just need to write a class that implements both the TimeTable interface and the CanScheduleService interface,” Tess says. Once again, the pair use a test-first strategy to imagine and implement a TimeTable class. They decide to start with a simple implementation called InMemoryTimeTable. They start off with an empty implementation like this one:

公共类 InMemoryTimeTable 实现 TimeTable、CanScheduleServices {
 
    @Override
    公共无效的scheduleService(字符串行,
                                列表<LocalTime> 出发时间,
                                弦乐的离去,
                                字符串目标){}
 
    @Override
    公共列表<String> findLinesThrough(字符串从,字符串到) {
        返回空值;
    }
 
    @Override
    公共列表<LocalTime> getDepartures(字符串 lineName,字符串 from){
        返回空值;
    }
}
public class InMemoryTimeTable implements TimeTable, CanScheduleServices {
 
    @Override
    public void scheduleService(String line,
                                List<LocalTime> departingAt,
                                String departure,
                                String destination) {}
 
    @Override
    public List<String> findLinesThrough(String from, String to) {
        return null;
    }
 
    @Override
    public List<LocalTime> getDepartures(String lineName, String from) {
        return null;
    }
}

由于他们选择了模块化设计,以后很容易改进这个实现,甚至可以用完全不同的实现来代替它。第一个测试侧重于调度服务,如下所示:

Thanks to the modular design they have chosen, it will be easy to evolve this implementation later on, or even replace it with a totally different one. The first test focuses on scheduling services, and looks like this:

@DisplayName("当安排火车服务时")
类 WhenRecordingTrainSchedules {
 
    // 给定
    InMemoryTimeTable timeTable = new InMemoryTimeTable();                
 
    @测试
    @DisplayName(“我们可以安排一次单一时间的行程”)
    void tripWithOneScheduledTime() {
        // 什么时候
        timeTable.scheduleService("T1", LocalTimes.at("09:15"),           
                                  “霍恩斯比”
                                  “中央”);
        // 然后
        断言(timeTable.getDepartures("T1", "Hornsby"))。             
            .hasSize(1);
    }
}
@DisplayName("When scheduling train services")
class WhenRecordingTrainSchedules {
 
    // Given
    InMemoryTimeTable timeTable = new InMemoryTimeTable();               
 
    @Test
    @DisplayName("We can schedule a trip with a single scheduled time")
    void tripWithOneScheduledTime() {
        // When
        timeTable.scheduleService("T1", LocalTimes.at("09:15"),          
                                  "Hornsby",
                                  "Central");
        // Then
        assertThat(timeTable.getDepartures("T1", "Hornsby")).            
            .hasSize(1);
    }
}

创建新的时间表

Creates a new timetable

安排单一出发时间的服务

Schedules a service with a single departure time

查看预定的出发时间

Checks the scheduled departure times

InMemoryTimeTable这个测试要求这对搭档向类中添加一些数据结构他们决定将预定的行程存储在一张地图中,并按线路名称索引:

This test leads the pair to add some data structure to the InMemoryTimeTable class. They decide to store the scheduled trips in a map, indexed by line name:

公共类 InMemoryTimeTable 实现 TimeTable、CanScheduleServices {
    私有 Map<String, ScheduledService> schedules = new HashMap<>();       
 
    @Override
    公共无效的scheduleService(字符串行,
                                列表<LocalTime> 出发时间,
                                字符串来自,
                                字符串到){
        时间表.put(行, 
                      新的 ScheduledService(从,到,离开));          
    }
}
public class InMemoryTimeTable implements TimeTable, CanScheduleServices {
    private Map<String, ScheduledService> schedules = new HashMap<>();      
 
    @Override
    public void scheduleService(String line,
                                List<LocalTime> departingAt,
                                String from,
                                String to) {
        schedules.put(line, 
                      new ScheduledService(from, to, departingAt));         
    }
}

将预定的服务存储在按线路名称索引的地图中

Stores scheduled services in a map indexed by line name

记录预约的服务

Records the scheduled service

他们还决定将预定的服务表示为领域类,如下所示:

They also decide to represent scheduled services as a domain class, like this one:

公共类 ScheduledService {
    私人最终字符串离开;
    私有最终字符串目标;
    私人最终列表<LocalTime>出发时间;
 
    公共 ScheduledService(字符串来自,字符串到,列表 <LocalTime> 在){
        此.出发 = 来自;
        这个.目的地=到;
        这个.出发时间 = 在;
    }
       ...
}
public class ScheduledService {
    private final String departure;
    private final String destination;
    private final List<LocalTime> departureTimes;
 
    public ScheduledService(String from, String to, List<LocalTime> at) {
        this.departure = from;
        this.destination = to;
        this.departureTimes = at;
    }
       ...
}

这足以让代码通过。而且它足够简单,所以他们决定暂时不需要重构。

This is enough to make the code pass. And it is simple enough, so they decide that no refactoring is necessary yet.

现在他们对代码很满意,他们继续探索时间表的行为,检查当您安排多个出发时间以及添加多条线路时会发生什么。每次他们重复这个循环,编写一个小测试,使其失败,然后检查他们的代码以寻找潜在的改进。在通过几次测试后,他们对线路调度的有效性感到满意正确。

Now that they are happy with the code, they continue to explore the timetable behavior, checking what happens when you schedule several departure times and when you add more than one line. Each time they repeat the cycle, writing a small test, making it fail, and then reviewing their code to look for potential improvements. After a few more passing tests, they are satisfied that line scheduling works correctly.

实施时间表服务

Implementing the timetable service

“所以,“我们完成了,对吧?”Dave 说。“别急,”Tess 说。“我们几乎忘记了TimeTable接口方法。”

“So, we’re done, right?” says Dave. “Not so fast,” says Tess. “We almost forgot about the TimeTable interface methods.”

Tess 是对的。这足以使给定的步骤发挥作用,但不足以使整个场景发挥作用。他们的下一个工作是编写一个测试来探索findLinesThrough()getDepartures()方法“让我们从一个简单的例子开始,找到一条经过两个车站的线路,”戴夫:

Tess is right. This is enough for the given step to work, but not for the scenario as a whole. Their next job is to write a test that explores the findLinesThrough() and getDepartures() methods. “Let’s start with the simple case of finding a line that goes through two stations,” proposes Dave:

@测试
@DisplayName("查询火车服务时")
类 WhenQueryingTrainServices {
    // 给定
    InMemoryTimeTable timeTable = new InMemoryTimeTable();
 
    @测试
    @DisplayName("我们可以询问哪些线路经过任意两个车站")
    void queryLinesThroughStations() {
        // 什么时候
        时间表.scheduleService(“T1”,
                                  LocalTimes.at("09:15"),
                                  霍恩斯比、中央);
        // 然后
        assertThat(timeTable.findLinesThrough(“霍恩斯比”, 
                                “中央”))hasSize(1);
    }    
}
@Test
@DisplayName("When querying train services")
class WhenQueryingTrainServices {
    // Given
    InMemoryTimeTable timeTable = new InMemoryTimeTable();
 
    @Test
    @DisplayName("We can ask which lines go through any two stations")
    void queryLinesThroughStations() {
        // When
        timeTable.scheduleService("T1",
                                  LocalTimes.at("09:15"),
                                  "Hornsby", "Central");
        // Then
        assertThat(timeTable.findLinesThrough("Hornsby", 
                                "Central")).hasSize(1);
    }    
}

findLinesThrough()他们对该方法的初步实施看起来像这样:

Their initial implementation of the findLinesThrough() method looks like this:

    @Override
    公共列表<String> findLinesThrough(字符串从,字符串到) {
         时间表.entrySet()
                。溪流()
                .filter(line -> (line.getValue().getDeparture().equals(来自)          
                        && 行.getValue().getDestination().equals(to)))
                .map(Map.Entry::getKey)
                .收集(收集器.toList());
         }
    @Override
    public List<String> findLinesThrough(String from, String to) {
         schedules.entrySet()
                .stream()
                .filter(line -> (line.getValue().getDeparture().equals(from)          
                        && line.getValue().getDestination().equals(to)))
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());
         }

重构已完成的代码

Refactoring the completed code

测试通过了,但 Dave 并不信服。“这不是世界上最易读的代码,”他评论道。“我们看看能否重构一下,让它更容易理解一点。”

This makes the test pass, but Dave isn’t convinced. “It’s not the most readable code in the world,” he comments. “Let’s see if we can refactor it to make it a bit easier to follow.”

“也许我们可以整理一下过滤逻辑,”Tess 建议道。“我们要做的是找到一条或多条经过我们提供的两个车站的正确路线。”

“Maybe we could tidy up the filtering logic,” suggests Tess. “What we are trying to do is to find the line or lines that go through the two stations we provide, in the right direction.”

“如果我们只写这个会怎么样?”戴夫说。他修改了代码,想出了一些新方法,使他能够重构该linesGoThrough()方法使其更易读一些:

“What if we wrote just that?” says Dave. He tinkers with the code and comes up with a couple of new methods that allow him to refactor the linesGoThrough() method into something a little more readable:

私有 Set<String> lineNames() { return schedules.keySet(); }           
 
private boolean lineGoesThrough(String line, String from, String to){     
  返回 schedules.getOrDefault(line,ScheduledService.NO_SERVICE)
                  .goesBetween(从,到);
}
  
@Override
公共列表<String> findLinesThrough(字符串从,字符串到) {
  返回 lineNames().stream()
                    .filter(line -> lineGoesThrough(line,从,到))
                    .收集(收集器.toList());
}
private Set<String> lineNames() { return  schedules.keySet(); }          
 
private boolean lineGoesThrough(String line, String from, String to){    
  return schedules.getOrDefault(line, ScheduledService.NO_SERVICE)
                  .goesBetween(from,to);
}
  
@Override
public List<String> findLinesThrough(String from, String to) {
  return lineNames().stream()
                    .filter(line  -> lineGoesThrough(line, from, to))
                    .collect(Collectors.toList());
}

查找所有预定线路的名称

Finds the names of all the scheduled lines

一种检查给定预定线路是否在两个车站之间运行的便捷方法

A convenience method to check whether a given scheduled line goes between two stations

这也促使他重构了ScheduledService课程

This also leads him to refactor the ScheduledService class:

公共类 ScheduledService {
  私有最终字符串来自;
  私有最终字符串;
  私人最终列表<LocalTime>出发时间;
 
  公共静态 ScheduledService NO_SERVICE                              
                   =新的ScheduledService(“”,,“”,Lists.emptyList());
 
  公共 ScheduledService (字符串来自,字符串到,列表 <LocalTime> 在) {...}
 
  公共列表<LocalTime> getDepartureTimes() {
    返回出发时间;
  }
 
  public boolean goesBetween(String from, String to) {                   
    返回 this.from.equals(from) && this.to.equals(to);
  }
}
public class ScheduledService {
  private final String from;
  private final String to;
  private final List<LocalTime> departureTimes;
 
  public static ScheduledService NO_SERVICE                             
                   = new ScheduledService("","", Lists.emptyList());
 
  public ScheduledService(String from, String to, List<LocalTime> at) {...}
 
  public List<LocalTime> getDepartureTimes() {
    return departureTimes;
  }
 
  public boolean goesBetween(String from, String to) {                  
    return this.from.equals(from) && this.to.equals(to);
  }
}

表示没有出发时间的服务的常量值

A constant value representing a service with no departure times

一种方便检查特定服务是否在两个车站之间运行的便捷方法

A convenience method to make it easier to check that a given service goes between two stations

一旦他们通过了这一步,他们就会添加一个新测试来说明如何获取给定线路的出发时间:

Once they make this one pass, they add a new test that illustrates how to get the departure times of a given line:

  @测试
  @DisplayName("每条线路可以有多个出发时间")
  void trainLinesHaveMoreThanOneDepartureTime() {
    // 什么时候
    时间表.scheduleService(“T1”,
                LocalTimes.at("09:15","09:45"),
                “霍恩斯比”
                “中央”);
    // 然后
    断言(timeTable.getDepartures(“T1”,“霍恩斯比”))。hasSize(2);
  }
  @Test
  @DisplayName("Each line can have a number of departure times")
  void trainLinesHaveMoreThanOneDepartureTime() {
    // When
    timeTable.scheduleService("T1",
                LocalTimes.at("09:15","09:45"),
                "Hornsby",
                "Central");
    // Then
    assertThat(timeTable.getDepartures ( "T1", "Hornsby")).hasSize(2);
  }

等等。经过六次这样的小测试后,他们最终得到了InMemoryTimeTable班级,如以下清单所示。您还可以查看完整的测试类GitHub。

And so on. After half a dozen small tests like this, they end up with the InMemoryTimeTable class, as in the following listing. You can also see the full test classes on GitHub.

清单 3.6 完成的InMemoryTimeTable

Listing 3.6 The completed InMemoryTimeTable class

包 manning.bddinaction.timetables;
 
导入java.time.LocalTime;
导入 java.util.*;
导入java.util.stream.Collectors;
 
公共类 InMemoryTimeTable 实现 TimeTable、CanScheduleServices {
  私有 Map<String, ScheduledService> 计划 = 新 HashMap<>();
 
  @Override
  公共无效的scheduleService(字符串行,
                                列表<LocalTime> 出发时间,
                                字符串来自,
                                字符串到){
      时间表.put(行, 
                    新的 ScheduledService(从,到,出发点));     
  }
 
  私有 Set<String> lineNames() { 返回 schedules.keySet(); }
 
  私有布尔值lineGoesThrough(字符串线,字符串从,字符串到){
    返回 schedules.getOrDefault(line,ScheduledService.NO_SERVICE)
                        .goesBetween(从,到);
  }
  @Override
  公共列表<String> findLinesThrough(字符串从,字符串到) {
    返回 lineNames().stream()
                      .filter(line -> lineGoesThrough(line,从,到))
                      .收集(收集器.toList());
  }
 
  @Override
  公共列表<LocalTime> getDepartures(字符串 lineName,字符串 from){
    如果 (!schedules.containsKey(lineName)) {
      抛出新的UnknownLineException(“未找到行:”+lineName);
    }
    返回 schedules.get(lineName).getDepartureTimes();
  }
}
package manning.bddinaction.timetables;
 
import java.time.LocalTime;
import java.util.*;
import java.util.stream.Collectors;
 
public class InMemoryTimeTable implements TimeTable, CanScheduleServices {
  private Map<String, ScheduledService> schedules = new HashMap<>();
 
  @Override
  public void scheduleService(String line,
                                List<LocalTime> departingAt,
                                String from,
                                String to) {
      schedules.put(line, 
                    new ScheduledService(from, to, departingAt));     
  }
 
  private Set<String> lineNames() { return  schedules.keySet(); }
 
  private boolean lineGoesThrough(String line, String from, String to) {
    return schedules.getOrDefault(line, ScheduledService.NO_SERVICE)
                        .goesBetween(from,to);
  }
  @Override
  public List<String> findLinesThrough(String from, String to) {
    return lineNames().stream()
                      .filter(line  -> lineGoesThrough(line, from,to))
                      .collect(Collectors.toList());
  }
 
  @Override
  public List<LocalTime> getDepartures(String lineName, String from) {
    if (!schedules.containsKey(lineName)) {
      throw new UnknownLineException("No line found: " + lineName);
    }
    return schedules.get(lineName).getDepartureTimes();
  }
}

3.6 演示:测试作为动态文档

3.6 Demonstrate: Tests as living documentation

一次一旦实现了某个功能,您就应该能够运行测试并查看待定测试中通过的验收标准(见图 3.12)。当您应用 BDD 等实践时,此结果不仅仅告诉您应用程序满足了业务需求。通过验收测试也是进度的具体衡量标准。已实施的测试要么通过,要么失败。理想情况下,如果某个功能的所有验收标准都已自动化并成功运行,您可以说此功能已完成并准备好投入生产。

Once a feature has been implemented, you should be able to run your tests and see passing acceptance criteria among pending ones (see figure 3.12). When you’re applying practices like BDD, this result does more than simply tell you that your application satisfies the business requirements. A passing acceptance test is also a concrete measure of progress. An implemented test either passes or fails. Ideally, if all of the acceptance criteria for a feature have been automated and run successfully, you can say that this feature is finished and ready for production.

图 3.12 通过的测试现在应该出现在测试报告中。

Figure 3.12 The passing test should now appear in the test reports.

测试状态不仅可以评估应用程序的质量,还可以清楚地表明应用程序在开发过程中所处的位置。通过测试的比例与指定验收标准总数的比例可以很好地反映出迄今为止已完成的工作量以及剩余的工作量。此外,通过跟踪已完成的自动验收测试数量与待完成测试的数量,您可以了解自己在一段时间内取得的进展。

More than just evaluating the quality of your application, the state of the tests gives a clear indication of where it’s at in the development progression. The proportion of passing tests compared to the total number of specified acceptance criteria gives a good picture of how much work has been done so far and how much remains. In addition, by tracking the number of completed automated acceptance tests against the number of pending tests, you can get an idea of the progress you’re making over time.

当你用这种叙述风格编写测试时,另一个好处是:每个自动验收测试都成为一个记录在案的、可操作的示例,说明如何使用该系统来解决特定的业务需求。当测试是 Web 测试时,这些可操作的示例甚至会用沿测试进行截取的屏幕截图来说明。方式。

When you write tests in this narrative style, another benefit emerges: each automated acceptance test becomes a documented, worked example of how the system can be used to solve a particular business requirement. And when the tests are web tests, the worked examples will even be illustrated with screenshots taken along the way.

但测试人员怎么办?自动化验收测试和 QA

But what about the testers? Automated acceptance testing and QA

当自动验收测试通过后,自动将应用程序部署到生产环境中需要严格的纪律,并且对自动测试的质量和全面性有最大的信心。这是一个值得追求的目标,许多组织确实做到了这一点,但对于大多数组织来说,事情并不那么简单。

Automatically deploying your application into production when the automated acceptance tests pass requires a great deal of discipline and the utmost confidence in the quality and comprehensiveness of your automated tests. This is a worthy goal, and a number of organizations do manage this, but for most, things are not quite that simple.

在典型的企业环境中,测试人员可能仍希望在将应用程序发布到生产环境之前至少进行一些探索性测试。但如果自动化测试结果清晰可见,他们可以为 QA 团队节省通常用于回归或基本机械测试的几天或几周的时间,让他们专注于更有趣的测试活动。这反过来可以显著加快发布周期。

In typical enterprise environments, the testers will probably still want to do at least some exploratory testing before releasing the application to production. But if the automated test results are clear and visible, they can save the QA team days or weeks of time that would normally be spent on regression or basic mechanical testing and let them focus on more interesting testing activities. This, in turn, can speed up the release cycle significantly.

3.7 BDD 降低维护成本

3.7 BDD reduces maintenance costs

在许多组织中,参与初始项目的开发人员在应用程序投入生产后不再维护应用程序。相反,该任务被移交给维护或 BAU(照常运营)) 团队。在这种环境中,可执行规范和动态文档是简化交接过程的好方法,因为它们提供了一组应用程序功能的实例以及支持这些功能的代码说明。

In many organizations, the developers who worked on the initial project don’t maintain the application once it goes into production. Instead, the task is handed over to a maintenance or BAU (Business as Usual) team. In this sort of environment, executable specifications and living documentation are a great way to streamline the hand-over process, as they provide a set of worked examples of the application’s features and illustrations of the code that supports these features.

可执行规范还使维护团队更容易实施更改或错误修复。让我们通过一个简单的示例来了解它是如何工作的。假设用户要求被告知即将到达的下四趟列车,而不仅仅是接下来的两趟列车,就像目前的情况一样。与此要求相关的场景如下:

Executable specifications also make it much easier for maintenance teams to implement changes or bug fixes. Let’s see how this works with a simple example. Suppose that users have requested to be informed about the next four trains that are due to arrive, and not just the next two, as is currently the case. The scenario related to this requirement is as follows:

场景:下一班列车将前往同一线路上的请求目的地
    鉴于开往中央车站的 T1 列车于 08:02、08:15、08:21 从霍恩斯比出发
    Travis 想要在 08:00 从 Hornsby 前往 Chatswood 时
    然后他应该被告知火车的运行时间:08:02、08:15
Scenario: Next train going to the requested destination on the same line
    Given the T1 train to Central leaves Hornsby at 08:02, 08:15, 08:21
    When Travis wants to travel from Hornsby to Chatswood at 08:00
    Then he should be told about the trains at: 08:02, 08:15

这个场景表达了您当前对需求的理解:应用程序当前的行为如此,并且您有自动化验收标准和单元测试来证明这一点。

This scenario expresses your current understanding of the requirement: the application currently behaves like this, and you have automated acceptance criteria and unit tests to prove it.

但新的用户请求改变了这一切。现在的情况应该是这样的:

But the new user request has changed all this. The scenario now should be something like this:

场景:下一班列车将前往同一线路上的请求目的地
    鉴于前往 Chatswood 的 T1 列车于 08:02、08:15、08:21 从 Hornsby 出发, 
 8:34, 8:45
    Travis 想要在 08:00 从 Hornsby 前往 Chatswood 时
    然后他应该被告知以下时间的火车信息:08:02、08:15、08:21、08:34
Scenario: Next train going to the requested destination on the same line
    Given the T1 train to Chatswood leaves Hornsby at 08:02, 08:15, 08:21, 
 8:34, 8:45
    When Travis wants to travel from Hornsby to Chatswood at 08:00
    Then he should be told about the trains at: 08:02, 08:15, 08:21, 08:34

当您运行这个新场景时,它将失败(见图 3.13)。这很好!这表明应用程序没有执行要求它执行的操作。现在您有一个实施此修改的起点。

When you run this new scenario, it will fail (see figure 3.13). This is good! It demonstrates that the application doesn’t do what the requirements ask of it. Now you have a starting point for implementing this modification.

图 3.13 未通过的验收标准说明了需求所要求的内容与应用程序当前所做的事情之间的差异。

Figure 3.13 A failing acceptance criterion illustrates a difference between what the requirements ask for and what the application currently does.

从这里开始,您可以使用单元测试来隔离需要更改的代码。您将更新“应该提出接下来的 2 趟列车”单元测试以反映新的验收标准:

From here, you can use the unit tests to isolate the code that needs to be changed. You’ll update the “should propose the next 2 trains” unit test to reflect the new acceptance criterion:

@测试
@DisplayName(“应提议接下来的 4 趟列车”)
无效行程与多个预定时间(){
 
    时间表 
      = 出发(
          在(“8:10”),在(“8:20”),在(“8:30”),在(“8:45”),在(“8:45”));          
    行程 = 新的 ItineraryService(时间表);
 
    列表<LocalTime>proposedDepartures
       = itineraries.findNextDepartures(at("8:05"), "霍恩斯比", "中央");
    断言(提议出发)
       .containsExactly(在("8:10"),在("8:20"),在("8:30"),在("8:45"));     
    }
@Test
@DisplayName("should propose the next 4 trains")
void tripWithSeveralScheduledTimes() {
 
    timeTable 
      = departures(
          at("8:10"),at("8:20"),at("8:30"),at("8:45"),at("8:45"));         
    itineraries = new ItineraryService(timeTable);
 
    List<LocalTime> proposedDepartures
       = itineraries.findNextDepartures(at("8:05"), "Hornsby", "Central");
    assertThat(proposedDepartures)
       .containsExactly(at("8:10"), at("8:20"), at("8:30"),at("8:45"));    
    } 

假装服务现在返回更多行程。

The pretend service now returns more trips.

您现在预计行程服务返回四次。

You now expect the itinerary service to return four times.

ItineraryService这反过来又帮助你隔离类中需要更改的代码。从这里开始,您将能够更好地正确更新代码。

This, in turn, helps you isolate the code that needs to change in the ItineraryService class. From here, you’ll be in a much better position to update the code correctly.

对于较大的变更,显然需要做更多的工作。但对于任何规模的修改,原则都是一样的。如果变更请求是对现有功能的修改,则需要更新自动验收标准以反映新要求。如果变更是当前验收标准未捕获的错误修复,则需要首先编写新的自动验收标准来重现错误,然后修复错误,最后使用验收标准来证明错误已得到解决。如果变更大到使现有验收标准变得多余,则可以删除旧的验收标准并编写新的个。

For larger changes, more work will obviously be involved. But the principle remains the same for modifications of any size. If the change request is a modification of an existing feature, you need to update the automated acceptance criteria to reflect the new requirement. If the change is a bug fix that your current acceptance criteria didn’t catch, then you need to first write new automated acceptance criteria to reproduce the bug, then fix the bug, and finally use the acceptance criteria to demonstrate that the bug has been resolved. And if the change is big enough to make existing acceptance criteria redundant, you can delete the old acceptance criteria and write new ones.

概括

Summary

  • 了解项目的根本业务目标可以让您发现可以实现这些业务目标的功能和故事。

  • Understanding the underlying business objectives of a project lets you discover features and stories that can deliver these business objectives.

  • 功能描述了能够帮助用户和利益相关者实现其目标的功能。

  • Features describe functionality that will help users and stakeholders achieve their goals.

  • 功能可以分解为更易于一次性构建和交付的故事。

  • Features can be broken down into stories that are easier to build and deliver in one go.

  • 具体的例子是描述和讨论特征的有效方法。

  • Concrete examples are an effective way to describe and discuss features.

  • 以半结构化的“给定...何时...然后”符号表达的示例可以以自动验收标准的形式实现自动化。

  • Examples, expressed in a semi-structured Given ... When ... Then notation, can be automated in the form of automated acceptance criteria.

  • 验收标准推动低级实施​​工作,并帮助您设计和编写真正需要的代码。

  • Acceptance criteria drive the low-level implementation work and help you design and write only the code you really need.

  • 您还可以在单​​元测试中使用 BDD 风格的 Given ... When ... Then 结构。

  • You can also use the BDD-style Given ... When ... Then structure in your unit tests.

  • 自动验收标准还以动态文档的形式记录所交付的功能。

  • The automated acceptance criteria also document the features delivered, in the form of living documentation.

  • 自动化验收标准和 BDD 风格的单元测试使维护变得相当容易。

  • Automated acceptance criteria and BDD-style unit tests make maintenance considerably easier.

在接下来的章节中,我们将更详细地讨论每个主题,我们还将研究如何使用不同的工具和方法来将我们讨论的方法付诸实践。技术。

In the following chapters, we’ll discuss each of these themes in much more detail, and we’ll also look at how the approaches we discuss can be put into practice using different tools and technologies.


1  然后可以使用这些索引卡来规划和可视化您的进度。

1  These index cards can then be used to plan and visualize your progress.

2  这种格式最初由 Chris Matts 在功能注入的背景下提出,我们将在下一章中讨论。

2  This format was originally proposed by Chris Matts, in the context of Feature Injection, which we’ll look at in the next chapter.

3  您可以参考图 3.1 中的地图来理解这些示例。

3  You can follow along with these examples by referring to the map in figure 3.1.

4  Example Mapping 最初由 Matt Wynne 开发(参见https://cucumber.io/blog/example-mapping-introduction/)。

4  Example Mapping was originally developed by Matt Wynne (see https://cucumber.io/blog/example-mapping-introduction/).

5  正如我们在第 1 章中提到的,此处显示的 Given ... When ... Then 语法通常称为 Gherkin 格式。Gherkin 是 Cucumber 及其相关工具使用的语法,我们将在本示例中使用它。我们将在第 5 章中详细介绍这一切。

5  As we mentioned in chapter 1, the Given ... When ... Then syntax shown here is often referred to as the Gherkin format. Gherkin is the syntax used by Cucumber and related tools, which is what we will use for this example. We’ll look at all this in great detail in chapter 5.

6  如果您不喜欢 Java,请不要担心;代码示例旨在让任何具有一定编程背景的人都能阅读。

6  If Java isn’t your cup of tea, don’t worry; the code samples are designed to be readable by anyone with some programming background.

7  有关该库的更多详细信息,请参阅 Serenity BDD 网站 ( http://serenity-bdd.info )。

7  See the Serenity BDD site (http://serenity-bdd.info) for more details about this library.

8  本章的源代码可以在 GitHub 上找到(https://github.com/bdd-in-action/second-edition)。

8  The source for this chapter is available on GitHub (https://github.com/bdd-in-action/second-edition).

9  请参阅https://github.com/serenity-bdd/serenity-cucumber-starter

9  See https://github.com/serenity-bdd/serenity-cucumber-starter.

10  如果您不是常规 Maven 用户,Maven 将首先下载需要使用的库;这可能需要一些时间,但您只需执行一次。

10  If you’re not a regular Maven user, Maven will first download the libraries it needs to work with; this may take some time, but you’ll only need to do it once.

第 2 部分:我想要什么?使用 BDD 定义需求

Part 2. What do I want? Defining requirements using BDD

BDD原则适用于软件开发的所有级别,从需求发现和定义到低级编码和回归测试。本书第 2 部分重点介绍 BDD 的第一个方面:如何使用 BDD 发现和描述需要构建的功能。第 2 部分是为整个团队编写的。

BDD principles are applicable at all levels of software development, from requirements discovery and definition to low-level coding and regression testing. Part 2 of this book focuses on the first aspect of BDD: how you can use BDD to discover and describe the features you need to build. Part 2 is written for the whole team.

在第 4 章中,我们将探讨 BDD 在更高级别需求中的作用。您将看到考虑和理解您被要求交付的软件背后的业务动机和价值是多么重要。您将学习如何讨论所提议功能的相对价值,并利用这些讨论来确定要构建哪些功能,更重要的是,不要构建哪些功能。这是构建正确软件的核心。

In chapter 4, we’ll look the role BDD plays in higher-level requirements. You’ll see how important it is to consider and understand the business motivation and value behind the software you’re asked to deliver. You’ll learn how to discuss the relative value of proposed features and use these discussions to determine what features to build and, more importantly, what features not to build. This is at the heart of building the right software.

在第 5 章和第 6 章中,我们将进行更详细的介绍。您将了解 BDD 团队如何协作发现、描述和定义他们需要交付的功能。您将发现如何使用对话和具体示例探索功能的范围和详细要求,并且您将了解这些对话如何改变您对要定义的功能的理解。

In chapters 5 and 6, we’ll go into more detail. You’ll learn how BDD teams collaborate to discover, describe, and define the features they need to deliver. You’ll discover how to explore the scope and detailed requirements of a feature using conversation and concrete examples, and you’ll learn how these conversations can change your understanding of the features you’re trying to define.

最后,在第 7 章中,您将学习如何将第 6 章中讨论的示例以清晰、明确的形式形式化,我们将其称为可执行规范。这也是一个协作过程;正如您将看到的,以更严格的形式表达示例的行为有助于消除需求周围的歧义和假设,也为自动化铺平了道路。

Finally, in chapter 7, you’ll learn how to formalize the examples we discussed in chapter 6 in a clear, unambiguous form that we’ll refer to as executable specifications. This too is a collaborative process; as you’ll see, the act of expressing the examples in a more rigorous form helps eliminate ambiguities and assumptions around the requirements, and it also paves the way for automation.

在第 2 部分结束时,您应该对 BDD 团队为何以及如何协作来发现和定义他们需要交付的功能有充分的了解,并且您将能够以可执行规范的形式表达围绕这些功能的验收标准。

At the end of part 2, you should have a solid understanding of why and how BDD teams collaborate to discover and define the features they need to deliver, and you’ll be able to express the acceptance criteria around these features in the form of executable specifications.

4 推测:从业务目标到优先功能

4 Speculate: From business goals to prioritized features

本章封面

This chapter covers

  • 推测阶段会发生什么
  • What happens during the Speculate phase
  • BDD 和业务敏捷性
  • BDD and business agility
  • 描述项目愿景和业务目标
  • Describing a project vision and business goals
  • 使用影响图可视化假设并确定特征的优先级
  • Visualizing assumptions and prioritizing features with Impact Mapping
  • 使用海盗画布构建史诗般的风景
  • Building an Epic Landscape with Pirate Canvases

在实施软件解决方案之前,甚至在判断应该实施哪些功能之前,您需要了解要解决的问题以及您可以如何提供帮助。谁将使用该系统,他们期望从中获得什么好处?您的系统将如何帮助用户完成工作或为您的利益相关者提供价值?

Before you can implement a software solution, and before you can even judge what features you should implement, you need to understand the problem you’re solving and how you can help. Who will be using the system, and what benefits will they expect from it? How will your system help users do their jobs or provide value to your stakeholders?

您如何知道某个特定功能是否真的会像您认为的那样给组织带来好处?您开发的软件是否会对客户的业务产生可衡量的积极影响?您的项目会带来改变吗?有时,某个特定功能,甚至某个特定应用程序,都不应该实施,因为它显然不会带来预期的业务利益。

How can you know if a particular feature will really benefit the organization as you suppose it should? Are you building software that will have a measurable, positive impact for your client’s business? Will your project make a difference? Sometimes a particular feature, or even a particular application, shouldn’t be implemented because it will clearly not deliver the business benefits expected of it.

这些是我们在推测阶段会问的问题。在本章中,您将学习如何更深入地了解业务的高级需求,这是有效 BDD 实践的重要基石。您将了解 BDD 如何融入战略项目规划和高级需求发现,您将学习如何讨论和描述高级业务愿景和目标,以及两种技术,即影响图和海盗画布,它们可以帮助您确定可能实现这些目标的关键可交付成果。在下一章中,我们将研究 BDD 和产品待办事项细化活动,在这些活动中,我们将这些可交付成果分解为更易于管理的用户故事。在第 5 章中,我们将了解如何在说明阶段确定和探索这些故事的验收标准。

These are the sort of questions we ask during the Speculate phase. In this chapter you’ll learn how to gain a deeper understanding of high-level needs of the business, an essential cornerstone of effective BDD practices. You’ll learn about how BDD fits into strategic project planning and high-level requirements discovery, and you’ll learn how to discuss and describe high-level business vision and goals, as well as about two techniques, Impact Mapping and Pirate Canvases, that can help you identify the key deliverables that might deliver on these goals. In the next chapter, we will look at BDD and Product Backlog Refinement activities, where we break these deliverable features into more manageable User Stories. And in chapter 5, we see how to identify and explore the acceptance criteria of these stories in the Illustrate phase.

当 BDD 遇到业务敏捷性

When BDD meets business agility

推测阶段是 BDD 与业务敏捷性相遇的阶段。根据敏捷业务联盟 ( https://www.agilebusiness.org/ ) 的说法,业务敏捷性是指组织能够

The Speculate phase is where BDD meets business agility. According to the Agile Business Consortium (https://www.agilebusiness.org/), business agility is the ability of an organization to

  • 快速适应内部和外部的市场变化

  • Adapt quickly to market changes—internally and externally

  • 快速灵活地响应客户需求

  • Respond rapidly and flexibly to customer demands

  • 在不影响质量的情况下,以高效且经济的方式适应和引领变革

  • Adapt and lead change in a productive and cost-effective way without compromising quality

  • 持续保持竞争优势

  • Continuously be at a competitive advantage

业务敏捷性与价值观、行为和能力的演变有关。这些使企业和个人在处理复杂性、不确定性和变化时更具适应性、创造力和弹性,从而改善福祉并取得更好的结果。

Business agility is concerned with the adoption of the evolution of values, behaviors, and capabilities. These enable businesses and individuals to be more adaptive, creative, and resilient when dealing with complexity, uncertainty, and change, leading to improved well-being and better outcomes.

BDD 实践是业务敏捷性的基石,利用业务敏捷性是从 BDD 采用中获得最大价值的先决条件。BDD 为团队提供了在战略和团队层面进行协作所需的技术和工具,可执行规范和动态文档都有助于在交付功能时提供更快、更准确的反馈。BDD 的核心是提供价值;如果编写出色的可执行规范不能解决对业务有用的问题,那么编写出色的可执行规范也是毫无意义的。

BDD practices are a cornerstone of business agility, and leveraging business agility is a prerequisite for getting the most value out of your BDD adoption. BDD gives teams the techniques and tools teams need to collaborate both at a strategic and at a team level, and executable specifications and living documentation both help to provide faster and more accurate feedback about features as they are delivered. BDD at its heart is about delivering value; it’s no use writing great executable specifications if they don’t solve a problem that is useful to the business.

4.1 推测阶段

4.1 The Speculate phase

推测阶段涉及几项不同的活动,在项目生命周期的不同阶段执行(见图 4.1)。其中最重要的是战略规划和产品待办事项细化。

The Speculate phase involves several different activities, performed at different points in the project life cycle (see figure 4.1). The most important of these are Strategic Planning and Product Backlog Refinement.

图 4.1 推测阶段帮助团队和整个组织确定最重要的可交付成果并确定其优先顺序。

Figure 4.1 The Speculate phase helps teams, and organizations as a whole, identify and prioritize the deliverables that matter most.

在战略规划期间,您将定义关键业务目标并确定可能有助于实现这些目标的高级功能。然后,这些高级功能将在产品待办事项细化会议期间进行细化和优先排序,然后团队就可以选择并开始处理它们了。

During Strategic Planning, you define key business goals and identify the high-level features that might help achieve these goals. These high-level features are then refined and prioritized during Product Backlog Refinement sessions and are then ready for teams to pick them up and start work on them.

4.1.1 BDD 项目中的战略规划

4.1.1 Strategic Planning in a BDD project

战略规划是将战略性业务机会或挑战转化为可交付功能的优先集合。这些讨论中产生的高级功能构成了开发这些功能的团队的产品待办事项:

Strategic Planning is where you transform strategic business opportunities or challenges into a prioritized collection of deliverable features. The high-level features that come out of these discussions make up the product backlog for the team or teams building them:

  • 战略规划不只发生一次,而是在整个项目生命周期中反复发生。

  • Strategic Planning happens not just once, but repeatedly throughout the life of a project.

  • 活动涉及高层管理人员和利益相关者,以及将构建和交付功能的团队成员。

  • Activities involve both high-level executives and stakeholders, as well as members of the teams who will build and deliver the features.

  • 在战略规划期间,业务利益相关者在交付团队成员的支持下,确定并阐明需要解决的最重要的业务问题和最有希望探索的机会。

  • During Strategic Planning, business stakeholders, supported by delivery team members, identify and articulate the most important business problems that need solving and the most promising opportunities to explore.

  • 团队使用海盗画布和影响图等协作实践来识别商业价值和机会,了解哪些功能可以带来此价值,并强调假设和风险。

  • Teams use collaboration practices such as Pirate Canvases and Impact Mapping to identify business value and opportunities, to understand what features might deliver this value, and to highlight assumptions and risks.

让我们更详细地看一下这些要点细节。

Let’s look at each of these points in more detail.

4.1.2 战略规划是一项持续的活动

4.1.2 Strategic Planning is a continuous activity

为了在许多组织中,战略规划涉及高层利益相关者和高管,他们花费大量时间和精力就业务目标和目的达成一致,并制定 12 个月至 5 年之间的详细长期计划。

For many organizations, Strategic Planning involves high-level stakeholders and executives who spend a lot of time and effort agreeing on business goals and objectives and mapping out detailed, long-term plans anywhere between 12 months and 5 years.

但是,通常很难在超过三个月的时间范围内制定详细的计划。正如我们在第 1 章中讨论的那样,您很少会在项目开始时完全或正确地了解需求,而且需求和假设通常会在项目的生命周期中发生变化。当您对项目知之甚少时,在项目的初始阶段试图绘制详细的规范是没有成效的;这只会让您在以后的需求或您对需求的理解不可避免地发生变化时重新进行工作。相反,您应该尝试深入了解项目背后的业务环境,以便您可以对变化做出适当的反应,并继续专注于提供业务对项目所期望的价值。

However, it is generally difficult to plan in much detail beyond a time frame of around three months. As we discussed in chapter 1, you’ll rarely understand the requirements completely or correctly at the start of a project, and requirements and assumptions often change during the life of a project. It’s unproductive to try to draw detailed specifications in the initial phases of a project, when you know relatively little about it; this just sets you up for rework later, when the requirements, or your understanding of them, inevitably change. Instead, you should try to build a deep understanding of the business context behind the project so that you can react appropriately to change and remain focused on delivering the value that the business expects from the project.

这就是为什么对于敏捷组织来说,战略规划和需求发现是一个持续的过程,既发生在项目开始时,也发生在项目或开发产品的整个生命周期中。随着项目的进展,您可以观察市场条件的变化,吸取已发布功能的经验教训,并相应地调整您的优先事项。

This is why, for an Agile organization, Strategic Planning and requirements discovery is an ongoing process that occurs both at the start of a project and at regular intervals throughout the life of the project or product under development. As the project progresses, you look at changes in market conditions, take on board lessons learned from already released features, and adapt your priorities accordingly.

这些活动的频率因项目和组织而异。许多商店发现两到三个月的周期效果很好。对于实践 SAFe 的团队来说,这个节奏与 PI(程序增量)非常吻合) 规划活动(https://www.scaledagileframework.com/program-increment)。

The frequency of these activities varies from project to project and from organization to organization. Many shops find that two- to three-month cycles work well. For teams practicing SAFe, this cadence aligns nicely with PI (program increment) planning activities (https://www.scaledagileframework.com/program-increment).

其他团队,例如遵循 XSCALE 实践的团队(https://xscalealliance.org),工作节奏更频繁。一些商店发现,每周召开一次战略会议,让团队代表和高级利益相关者参与其中,可以更顺畅地协调和协调团队之间的合作。规模较小、更定期的会议可以更轻松地根据新信息调整和重新确定功能的优先级发现。

Other teams, such as those following XSCALE practices (https://xscalealliance.org), work with a more frequent cadence. Some shops find that weekly strategic meetings, involving both team delegates and senior stakeholders, allow for smoother coordination and alignment between teams. Smaller, more regular meetings make it easier to adjust and reprioritize features as new information is discovered.

敏捷组织是学习型组织

Agile organizations are learning organizations

实现更好业务成果的关键不是更快的编码,而是更快的学习。您需要能够更快地学习并更快地对所学内容做出反应。无论是在团队层面还是在组织层面,较小的反馈周期都是敏捷性的命脉。

The key to better business outcomes is not faster coding; it is faster learning. You need to be able to both learn more quickly and react to what you learn more quickly. Smaller feedback cycles are the lifeblood of agility, both at a team and at an organizational level.

4.1.3 战略规划涉及利益相关者和团队成员

4.1.3 Strategic Planning involves both stakeholders and team members

传统上这种高层规划是高级利益相关者、高管、领域专家和高级架构师的职责。这些讨论中提出的需求被写成功能规范并交给开发团队,而开发团队提供意见或反馈的机会有限。从开发团队的角度来看,需求发现始于用户故事或史诗的积压。

Traditionally this sort of high-level planning is the reserve of senior stakeholders, executives, domain experts, and senior architects. The requirements that come out of these discussions are written up as functional specifications and handed down to development teams, who have limited opportunities to provide input or feedback. And from the perspective of the development teams, requirements discovery starts with a backlog of User Stories or epics.

敏捷组织采用更具协作性的方法来发现需求。关于高层规划的讨论仍然需要高级利益相关者的参与,但也需要实际构建解决方案的团队成员的参与。

Agile organizations take a much more collaborative approach to requirements discovery. Discussions about high-level planning still involve senior stakeholders, but they also involve members of the teams who will actually be building the solutions.

当团队成员(或大型组织的团队代表)积极参与战略规划活动时,团队会对业务愿景和他们试图实现的目标有更深入的了解。他们还可以就所提想法的可行性提供宝贵的反馈。此外,这种更深入的理解使他们能够更有创造力地找到实现这些目标的最佳方法。

When team members (or team representatives, for larger organizations) actively participate in Strategic Planning activities, teams build up a much deeper understanding of the business vision and the goals they are trying to achieve. They can also provide valuable feedback about the feasibility of the proposed ideas. In addition, this deeper understanding allows them to be more creative in finding the best way to achieve these goals.

让团队成员参与高层次需求讨论乍一看似乎有些奇怪。但我合作过的最好的团队是这样的团队,团队成员已经积累了深厚的实践领域知识,并且了解他们需要处理的技术约束。开发人员和测试人员最初可能不具备业务领域知识,无法做出很大贡献,但他们会学习。让他们接触现实世界的用户需求和关注点是减少误解和错误假设的好方法,这些误解和错误假设可能会在以后导致问题项目。

Involving team members in high-level requirements discussions may seem odd at first. But the best teams I have worked with are the ones in which the team members have built up a deep, practical domain knowledge as well as an understanding of the technical constraints they will need to work with. Developers and testers might not have the business domain knowledge to contribute very much initially, but they will learn. And exposing them to real-world user needs and concerns is a great way to reduce misunderstandings and incorrect assumptions that can cause problems later in the project.

4.1.4 识别假设和假定,而不是特征

4.1.4 Identifying hypotheses and assumptions rather than features

我们之所以将这个阶段称为推测,是因为“推测”这个词提醒我们,任何新功能的价值都不是事先就能保证的。我们倾向于认为需求是业务部门一成不变的僵化指令,几乎没有改变的余地。我们倾向于假设业务人员知道他们想要什么,他们要求的确实是他们需要的。但我们往往假设得太多。

The reason we call this phase Speculate is that the word “speculate” reminds us that the value of any new feature is not guaranteed in advance. We tend to think of requirements as rigid dictates that are set in stone by the business, with little scope for change. We tend to assume that business folk know what they want, and that what they ask for is indeed what they need. But often we assume too much.

事实上,任何新功能都更像是一场赌注。我们投入开发时间和精力,希望该功能值得开发。我们打赌,我们开发的功能将带来足够的商业价值,以补偿我们为开发它所付出的努力。

In fact, any new feature is more like a bet. We are wagering development time and effort, in the hope that the feature will be worth building. We are betting that the feature we build will return enough business value to compensate for the effort we spend building it.

另一种思考方式是从假设的角度。早在 2011 年,Jeffery L. Taylor 就创造了“假设驱动开发”这一表达,1这是 DevOps 和当今精益创业方法中的一个关键概念。在假设驱动开发中,开发不被视为要实现的功能列表,而是一系列小型实验,用于验证或否定有关项目问题领域的假设。这些实验有助于我们逐步、渐进地增加对问题领域的理解。

Another way to think about this is in terms of hypotheses. Back in 2011, Jeffery L. Taylor coined the expression “Hypothesis-Driven Development,”1 which is a key concept in DevOps and the lean start-up methodology today. In Hypothesis-Driven Development, development is viewed not as a list of features to implement, but rather as a series of small experiments to validate or invalidate a hypothesis about the project’s problem domain. These experiments help us progressively and incrementally increase our understanding of a problem domain.

假设可以描述为一个简单的短语,描述信念或假设以及你打算如何验证它。一种流行的格式如下:

A hypothesis can be described as a simple phrase describing the belief or assumption and how you intend to verify it. One popular format goes like this:

我们相信<一些能力>

We believe <some capability>

将导致<某种结果>。

will result in <some outcome>.

当<观察到某些可测量的结果>时,我们就会知道这是正确的。

We will know this to be true when <some measurable result is observed>.

例如,如果您在一家网上书店工作,您可能会提出这样的假设:“我们相信,如果我们在主页上显示与客户之前购买的产品相关的产品,我们将提高参与度和销售额。当我们看到相关产品的销售额增加 5% 时,我们就会知道这是真的。”

For example, if you work for an online bookstore, you might come up with a hypothesis like this one: “We believe that if we show products related to a customer’s previous purchases on the home page, we will increase engagement and sales. We will know this to be true when we see a 5% increase in sales of related products.”

此类假设旨在被证实或被证伪,这就是我们将其与成功标准或指标联系起来的原因。以假设的方式思考会鼓励我们找出最简单的方法来证实或证伪每个假设。例如,您可以实现“相关产品”功能的简单版本,方法是显示客户已经购买的作者的其他书籍,并查看客户的反应。

Hypotheses like these are designed to be proved or disproved, which is why we associate them with success criteria or metrics. Thinking in terms of hypotheses encourages us to figure out the simplest way to prove or disprove each hypothesis. For example, you could implement a simple version of the “related products” feature by showing other books from authors that a customer has already purchased and see how your customers react.

部署功能后,您会在验证阶段收集相应的指标。这些指标将输入到下一个推测阶段(见图 4.2)。

When a feature is deployed, you collect the corresponding metrics during the validate phase. These metrics are fed into the next Speculate phase (see figure 4.2).

图 4.2 推测和验证阶段充当反馈循环,其中关于特征的假设和假定一旦被交付就可以得到验证。

Figure 4.2 The Speculate and Validate phases act as feedback loops where hypotheses and assumptions about features can be validated once they have been delivered.

在构建功能时牢记目标还可以帮助您思考验证产品所需的指标。由于团队已经理解并同意假设目标(在本例中,将相关产品的销售额提高 5%),并确定了指标(相关产品的销售额),因此您现在可以证明或反驳初始假设,并验证新功能是否实现了预期结果。

Building features with the goal in mind also helps you think about the metrics you will need to use to validate the product. Because the team has already understood and agreed on the hypothesis goal (in this case, increasing sales of related products by 5%), and identified the metric (sales of related products), you can now prove or disprove the initial hypothesis and validate whether the new feature delivered the expected results.

根据结果​​,您可能会决定继续开发和增强某项功能、通过另一项实验调整方法,甚至完全放弃该功能。专注于持续的实验周期并定期验证您对问题领域和解决方案的理解有助于您提供更有价值的软件。

Depending on the results, you might decide to continue to develop and enhance a feature, adjust the approach with another experiment, or even abandon the feature entirely. Focusing on a continuous experimentation cycle and on regularly validating your understanding of both the problem domain and the solution helps you deliver more valuable software.

在以下部分中,我们将介绍一些有助于我们理解业务目标、识别和优先考虑假设的实践。但在介绍海盗画布和影响图等有助于我们做到这一点的技术之前,我们需要了解如何描述业务愿景和目标更有效。

In the following sections, we will look at some of the practices that can help us understand business goals and identify and prioritize hypotheses. But before we look at techniques such as Pirate Canvases and Impact Mapping that can help us do this, we need to look at how we can describe business visions and goals more effectively.

4.2 描述业务愿景和目标

4.2 Describing business vision and goals

首先,让我们介绍您将在本书的其余部分与之合作的客户:Flying High Airlines。Flying High Airlines 是一家大型商业航空公司,运营国际和国内航班。由于成本增加和来自低成本航空公司的竞争,Flying High 一直面临压力,因此管理层最近推出了一个新版的飞行常客计划,以试图留住现有客户并吸引新客户。这个新计划将提供许多令人信服的加入理由;与所有飞行常客计划一样,会员在飞行时会积累积分,但会员还将享受许多专属特权,例如进入休息室和更快的登机队列,并且他们可以轻松地将积累的里程用于航班和为自己或家人购买其他物品。

Let’s start by introducing the client you’ll work with throughout the rest of the book: Flying High Airlines. Flying High Airlines is a large commercial airline that runs both international and domestic flights. Flying High has been under pressure due to increasing costs and competition from low-cost carriers, so management has recently launched a new and improved version of their Frequent Flyer program to try to retain existing customers and attract new ones. This new program will offer many compelling reasons to join; like all Frequent Flyer programs, members will accumulate points when they fly, but members will also benefit from many exclusive privileges, such as access to lounges and faster boarding lines, and they’ll be able to easily spend their accumulated miles on flights and on other purchases for themselves or their family members.

作为该计划的一部分,管理层希望建立一个新网站,让常旅客会员可以实时查看其当前状态、兑换积分和预订航班。现有系统每月仅向会员发送纸质帐户对帐单,告知他们已积累了多少积分。此外,Flying High 呼叫中心目前电话负荷过重,因为常旅客会员只有通过电话预订才能享受会员特权并使用累积的积分。管理层希望,能够直接在线预订而不是通过电话预订,这将鼓励常旅客会员更频繁地通过 Flying High 预订。在本章以及本书的其余部分中,我们将使用此项目中的示例来说明我们讨论的概念和技术。

As part of this initiative, management wants a new website where Frequent Flyer members can see their current status in real time, redeem points, and book flights. The existing system just sends out paper account statements to members each month to tell them how many points they’ve accumulated. In addition, the Flying High call center is currently overloaded with calls, as Frequent Flyer members can only benefit from their member privileges and use their accumulated points if they book over the phone. Management hopes that being able to book directly online instead of over the phone will encourage Frequent Flyer members to book more often with Flying High. In this chapter, and throughout the rest of the book, we’ll use examples from this project to illustrate the concepts and techniques we discuss.

4.2.1 愿景、目标、能力和特点

4.2.1 Vision, goals, capabilities, and features

一个实践 BDD 的团队与业务利益相关者合作,阐明和理解业务的愿景和目标。他们试图确定可能允许用户和其他利益相关者实现这些目标的功能,以及可能实现这些功能的软件特性。他们使用关于现实世界示例和反例的对话来更好地理解这些功能应该如何工作。图 4.3 说明了这些概念之间的关系,以及我们常旅客项目中的几个示例。

A team practicing BDD collaborates with business stakeholders to articulate and understand a business’s vision and goals. They try to identify capabilities that might allow users and other stakeholders to achieve these goals, and software features that might enable these capabilities. And they use conversations about real-world examples and counterexamples to better understand how these features should work. The relationship between these concepts is illustrated, along with a few examples from our Frequent Flyer project, in figure 4.3.

图 4.3 所有功能,最终是所有代码,都应该映射回业务目标和项目愿景。

Figure 4.3 All features, and ultimately all code, should map back to business goals and the project vision.

最高层是项目愿景,这是一份简短的声明,为项目提供了高层次的指导方向。这份声明有助于确保所有团队成员都了解项目的主要目标和假设。对于 Flying High Frequent Flyer 网站来说,愿景可能是建立一个忠实的客户群,这些客户积极地喜欢乘坐我们的航班。

At the highest level is the project vision, a short statement that provides a high-level guiding direction for the project. This statement helps ensure that all the team members understand the principal aims and assumptions of the project. For the Flying High Frequent Flyer website, the vision could be to build a loyal base of customers who actively prefer to fly with us.

更具体地说,任何项目都旨在支持一系列业务目标。业务目标是高管级别的概念,通常涉及增加收入、保护收入或降低成本(“业务价值”由此而来)。对于 Flying High Frequent Flyer 网站,主要业务目标之一可能是“通过常旅客会员的回头客赚取更多机票销售收入”。

More specifically, any project aims to support a number of business goals. Business goals are executive-level concepts that generally involve increasing revenue, protecting revenue, or reducing costs (this is where the “business value” comes from). For the Flying High Frequent Flyer website, one of the primary business goals might be “Earn more ticket sales revenue through repeat business from Frequent Flyer members.”

作为软件开发人员,您的工作是设计和构建可帮助企业实现这些目标的功能。无论实施情况如何,功能都可让您的用户实现某些目标或完成某些任务。Liz Keogh 说,识别功能的一个好方法是在其前面加上“能够”一词。例如,Flying High Frequent Flyer 会员需要能够在搭乘我们的航班时累积福利,或者能够预订航班并享受会员特权。

As a software developer, your job is to design and build capabilities that help the business realize these goals. A capability gives your users the ability to achieve some goal or fulfill some task, regardless of implementation. Liz Keogh says that a good way to spot a capability is that it can be prefixed with the words “to be able to.” For example, Flying High Frequent Flyer members need the capability to be able to cumulate benefits when they fly with us, or to be able to book flights and benefit from their member privileges while doing so.

功能并不意味着特定的实现。它们甚至不需要使用软件来实现,尽管软件可能会使它们更有效率。例如,“预订航班的能力”可以通过在线或手动电话提供。

Capabilities don’t imply a particular implementation. They don’t even need to be done using software, though software might make them more efficient. For example, “the ability to book a flight” could be provided online or manually, over the telephone.

您设计并实现功能来提供这些功能。功能是您实际构建的,它们才是价值所在。与功能不同,功能通常代表可交付的软件功能。一些可能有助于我们提供预订航班功能的功能可能是“每次完成航班赚取积分”和“使用飞行常客积分在线预订航班”。

You design and implement features to deliver these capabilities. Features are what you actually build, and they’re what deliver the value. Unlike capabilities, features generally represent pieces of deliverable software functionality. Some features that might help us deliver the capability to book flights might be “Earn points for each completed flight” and “Book flights online using Frequent Flyer points.”

一个功能通常太大而无法一次性交付,因此交付团队通常会将该功能拆分成更易于管理的部分,我们称之为用户故事。用户故事提供了说明功能的示例子集。您可以以可执行规范的形式自动执行这些示例,以加快反馈速度并从自动回归测试中受益。我们将在第 5 章中介绍如何将功能拆分成用户故事,以便更轻松地交付。

A feature is often too big to deliver in one go, so the delivery team typically slices the feature up into more manageable chunks, which we call User Stories. A User Story delivers a subset of the examples that illustrate a feature. You can automate these examples in the form of executable specifications in order to speed up feedback and benefit from automated regression tests. We will look at slicing features into User Stories to make them easier to deliver in chapter 5.

随着项目的进展,团队需要更多地了解他们决定交付的功能。为了更详细地了解某个功能,您可以使用具体示例来说明系统在不同情况下应该做什么。这些示例说明了某个功能的关键验收标准。

As the project progresses, teams need to learn more about the features they decide to deliver. To build up a more detailed understanding about a feature, you can use concrete examples of what the system should do in different situations. These examples illustrate the key acceptance criteria of a feature.

这不是预先完成的,而是在功能开始工作之前不久,作为说明阶段的一部分. 我们将在第 5 章中更详细地讨论在说明阶段使用示例和验收标准描述特征。

This isn’t done upfront, but shortly before work starts on a feature, as part of the Illustrate phase. We will discuss describing features with examples and acceptance criteria during the Illustrate phase in much more detail in chapter 5.

在本章的其余部分,我们将了解如何使用两种实用技术,即影响图和海盗画布,来发现和讨论您需要构建的功能和特性。但首先,让我们再谈谈项目愿景和业务目标。

In the rest of the chapter, we will see how you can use two practical techniques, Impact Mapping and Pirate Canvases, to discover and discuss the capabilities and features you need to build. But first, let’s talk a little more about project visions and business goals.

4.2.2 你想实现什么?从愿景开始

4.2.2 What do you want to achieve? Start with a vision

认为我本来想请你帮我买一个新的电钻,用于我在家里做一些 DIY 工作。我不知道现在电钻要多少钱,但我肯定不想花太多钱。红色的电钻不错,因为它和我的红色工具箱很相配。

Suppose I was to ask you to buy me a new power drill for some DIY jobs I need to do around the house. I’m not sure how much a power drill costs these days, but I certainly don’t want to spend more than I have to. A red one would be nice, as it would go with my red toolbox.

作为一个乐于助人的人,你会开车去当地的五金店,看看店里陈列的各种电钻。但如果不多介绍一些背景知识,你很难让我满意。如果我要用很多螺丝来组装一张大床,一个小而便宜的无绳电钻,甚至可能是无绳螺丝刀,就足够了,而较重的型号只会很笨重。另一方面,如果我要在车库的砖墙上安装一些新架子,我需要一个更强大的锤钻,以及一套石工钻头和一些螺丝锚。如果车库没有电源插座,钻头就必须是更昂贵的无绳钻头。

Being an obliging sort of person, you drive down to the local hardware store and look at the range of power drills on display. But without a bit more background, you’d be hard-pressed to make me happy. If I’m trying to assemble a large bed with a lot of screws, a small and inexpensive cordless electric drill, or maybe even a cordless screwdriver, should suffice, and a heavier model would just be cumbersome. On the other hand, if I’m going to install some new shelves on a brick wall in my garage, I’ll need a more powerful hammer drill, as well as a set of masonry drill bits and some screw anchors. If the garage has no power outlets, the drill will have to be of the more expensive cordless variety.

为了有效地完成工作,你需要清楚地了解你想要实现的目标以及工作的最终目标或目的。研究表明,当成员拥有一个明确的目标时,团队协作会显著改善。2对于软件开发来说,这同样适用。如果你要为客户面临的问题提供有效的解决方案,你需要了解潜在的业务目标。

To get things done effectively, you need a clear understanding both of what you’re trying to achieve and of the ultimate goal or purpose of your work. Studies have demonstrated that team collaboration improves significantly when members share a clearly identifiable goal.2 This is equally true for software development. If you are to deliver effective solutions for the problems your clients face, you need to understand the underlying business goals.

4.2.3 愿景陈述

4.2.3 The vision statement

表达项目应该实现的目标的有效方法是撰写愿景声明,该声明用几个简洁而引人注目的短语概述了项目的预期成果和目标。它让团队了解他们想要实现的目标,帮助激励他们并将他们的努力集中在产品的基本价值主张上。它为所有人提供了一盏明灯,帮助团队调整方向并集中精力构建积极有助于实现项目目标的功能。

One useful way to express what a project is supposed to achieve is to write a vision statement, which outlines the expected outcomes and objectives of the project in a few concise and compelling phrases. It gives the team an understanding of what they’re trying to achieve, helping to motivate them and focus their efforts on the essential value proposition of the product. It provides a beacon for all to see, helping teams align their direction and concentrate their efforts on building features that actively contribute to the project’s goals.

提示愿景声明应该简单、清晰、简洁——简短到几分钟内就能读完并理解。它还应该让整个团队非常熟悉。事实上,在迭代过程中,当需求快速频繁地发生变化时,团队很容易被细枝末节分散注意力,而愿景声明可以提醒他们项目的更广泛目标。

Tip The vision statement should be simple, clear, and concise—short enough to read and assimilate in a few minutes. It should also be intimately familiar to the whole team. Indeed, in the midst of an iteration, when requirements are changing quickly and frequently, it’s all too easy for teams to get sidetracked by minor details, and the vision statement can remind them about the broader goals of the project.

一份好的愿景声明将重点关注项目的目标,而不是如何实现这些目标。它不会详细介绍应使用哪种技术、项目交付应遵循什么时间框架或应在什么平台上运行。相反,它会根据项目试图解决的问题来介绍项目目标。

A good vision statement will focus on the project’s objectives, not on how it will deliver these objectives. It won’t go into detail about what technology should be used, what time frame the project delivery should respect, or what platform it should run on. Rather, it presents the project objectives in the context of the problem the project is trying to address.

大多数传统项目通常都有某种愿景。执行经理通常至少在内心深处知道他们对项目在商业价值方面的期望,否则他们不会批准该项目。根据其工作性质,高管通常非常清楚项目预计如何为盈利做出贡献。尽管如此,他们可能没有以一种可以轻松与开发团队分享的方式阐明预期结果。

Most traditional projects generally have a vision of some sort. Executive managers usually know, at least in the back of their minds, what they expect out of a project in terms of business value, or they wouldn’t have approved it. By the nature of their jobs, executives usually have a very keen understanding of how a project is expected to contribute to the bottom line. Despite this, they may not have articulated the expected outcomes in a way that can be easily shared with the development team.

大型组织通常需要某种正式文件来解释项目的业务案例和范围,而这种文件通常包含某种愿景声明。但这些文件通常是枯燥、死板的文件,是为了尊重当地项目管理流程要求而编写的,一旦项目获得批准,它们就会被交给项目管理办公室。它们是项目经理和项目管理办公室 (PMO) 的职责范围。在许多情况下,这些文件从未送到开发团队手中,这实际上使它们作为敏捷意义上的愿景声明毫无用处。事实上,如果不是每个人都能看到,那么共享愿景怎么会有用呢?它?

Larger organizations typically require some sort of formal documentation that explains the business case and scope of a project, and this document will usually contain a vision statement of sorts. But these are often dry, rigid documents, written to respect the local project management process requirements, and they’re relegated to a project management office once the project has been signed off on. They’re the realm of project managers and project management offices (PMOs). In many cases, these documents never make it to the development team, which effectively renders them useless as vision statements in the Agile sense. Indeed, how can a shared vision be useful if not everyone can see it?

4.2.4 使用愿景陈述模板

4.2.4 Using vision statement templates

一个项目愿景陈述不必是一份冗长冗长的文件。它可以是一个简洁的段落,用一两句话概括项目的重点。在他的书《跨越鸿沟:向主流客户营销和销售高科技产品》中(Harper Business,2002 年),Geoffrey A. Moore提出了以下良好产品愿景声明的模板,我在敏捷圈子里经常听到提到这个模板:

A project vision statement need not be a long and wordy document. It can be a succinct paragraph that sums up the focus of the project in one or two sentences. In his book Crossing the Chasm: Marketing and Selling High-Tech Products to Mainstream Customers (Harper Business, 2002), Geoffrey A. Moore proposes the following template for a good product vision statement, which I’ve often heard mentioned in Agile circles:

面向<目标客户>                                   
WHO <需要某样东西>                                   
<产品名称> 属于 <产品类别>              
那<关键优势,令人信服的购买理由>            
与<主要竞争替代方案>不同               
我们的产品<主要差异化声明>      
FOR <target customer>                                  
WHO <needs something>                                  
THE <product name> is a <product category>             
THAT <key benefit, compelling reason to buy>           
UNLIKE <primary competitive alternative>               
OUR PRODUCT <statement of primary differentiation>     

谁会购买(或受益于)该产品?

Who will buy (or benefit from) the product?

他们需要什么才能让他们想购买它?

What do they need that would make them want to buy it?

你到底提出了什么建议?

What sort of thing are you proposing, exactly?

是什么让它这么酷?

What makes it so cool?

你的竞争对手是什么?

What are you competing against?

为什么客户会喜欢您的解决方案?

Why would customers prefer your solution?

例如,Flying High 的飞行常客计划的愿景声明可能是这样的:

For example, the vision statement for Flying High’s Frequent Flyer program might go something like this:

对于旅行者
谁希望因搭乘 Flying High Airlines 旅行而获得奖励
Flying High 常旅客计划是一项忠诚度计划
让会员轻松便捷地查看和管理其累积的 
实时积分,并使用积分进行实际购买 
无与伦比的轻松。
与其他航空公司的飞行常客计划不同,
我们的产品让会员可以轻松地使用他们的积分进行任何类型的在线或 
实体购买。
FOR travelers
WHO want to be rewarded for traveling with Flying High Airlines
THE Flying High Frequent Flyer program IS A loyalty program
THAT lets members easily and conveniently view and manage their accumulated 
 points in real time, and spend their points for real purchases with 
 unequaled ease.
UNLIKE other airline Frequent Flyer programs,
OUR PRODUCT lets members use their points easily for any sort of online or 
 brick-and-mortar purchase.

这段简短的文字概括了您的目标受众是谁、他们想要什么、您的产品将如何满足他们的需求以及您的产品如何区别于竞争对手。当然,您不必完全遵循此模板(或任何其他模板),但它确实很好地概括了您应该考虑在愿景声明中包括哪些内容。

This short text sums up who your target audience is, what they want, how your product will give it to them, and how your product distinguishes itself from its competitors. Of course, there’s no obligation to follow this (or any other) template to the letter, but it does provide a good summary of the sort of things that you should consider including in your vision statement.

撰写可行的愿景陈述可能是一项棘手的工作,但它会在项目的整个生命周期内带来回报。撰写一份好的愿景陈述的一个有效方法是从设计产品传单的角度来思考。简而言之,你的产品是做什么的?产品将如何使你的组织受益?你的目标受众是谁,为什么他们会购买你的产品而不是竞争对手的产品?它的三个或四个主要卖点是什么?与所有关键项目参与者讨论这些问题,包括用户、利益相关者和开发团队。然后尝试使用 Moore 的模板制定愿景陈述。如果你的项目团队足够大,你可以分成几个跨职能小组并比较结果,不断完善陈述,直到每个人都同意同意。

Writing a viable vision statement can be a tricky exercise, but it pays off over the life of the project. One effective approach to writing a good vision statement is to think in terms of designing a product flyer. In a nutshell, what does your product do? How will the product benefit your organization? Who is your target audience, and why would they buy your product rather than that of a competitor? What are its three or four principal selling points? Discuss these questions with all the key project players, including users, stakeholders, and the development team. Then try to formulate a vision statement using Moore’s template. If your project team is big enough, you can split up into several cross-functional groups and compare results, refining the statement until everyone agrees.

4.2.5 这将如何使企业受益?确定业务目标

4.2.5 How will it benefit the business? Identify the business goals

一次您对项目愿景有清晰的认识,您需要定义推动项目并有助于实现这一愿景的底层业务目标。业务目标简洁地定义了项目将如何使组织受益或如何与组织的战略或使命保持一致。

Once you have a clear idea of the project vision, you need to define the underlying business goals that drive the project and contribute to realizing this vision. A business goal succinctly defines how the project will benefit the organization or how it will align with the organization’s strategies or vocation.

所有项目都有业务目标;否则,管理层一开始就不会批准这些目标。但并非所有项目都有明确定义和可见的业务目标。具有明确定义和良好沟通的业务目标的项目比没有的业务目标的项目成功的可能性要大得多。最近的一项大学研究发现,对目标有明确共同理解的团队合作得更好,工作效率更高。3

All projects have business goals; otherwise, management wouldn’t have approved them in the first place. But not all projects have clearly defined and visible business goals. And projects with well-defined and well-communicated business goals have a much better chance of success than those that don’t. A recent university study found that teams with a clear common understanding of their goal cooperated better and worked more effectively.3

当出现无法预见的问题、技术挑战使得特定解决方案的实施比最初想象的更困难时,或者当团队意识到他们误解了需求并需要以不同的方式做事时,理解这些目标就显得更加重要。如果希望开发人员能够适当地应对此类挑战,他们需要对系统的商业价值有一个透彻的理解。

Understanding these goals is even more important when unforeseen problems arise, when technical challenges make implementing a particular solution harder than initially thought, or when the team realizes they’ve misunderstood the requirements and need to do things differently. If developers are expected to respond appropriately to these sort of challenges, they need to have a solid understanding of what business value is expected from the system.

归根结底,业务人员希望所开发的软件能够帮助他们实现业务目标。如果软件在这方面表现出色,那么企业就会认为它是成功的,即使其范围和实施与最初的设想有很大不同。但如果软件未能满足基本的业务目标,那么即使它满足了客户提出的各种要求,也应该被视为失败。信。

At the end of the day, businesspeople want the software being built to help them achieve their business goals. If the software delivers in this regard, the business will consider it a success, even if the scope and implementation vary considerably from what was originally imagined. But if the software fails to meet the underlying business goals, then it will rightly be considered a failure, even if it meets the requirements provided by the customers down to the letter.

4.2.6 制定良好的商业目标

4.2.6 Writing good business goals

业务目标是侧重于业务价值或机会的高层陈述,是高管能够理解和讨论的陈述。例如,最重要的业务目标之一前面讨论过的飞行常客计划的目标可能如下:

Business goals are high-level statements that focus on business value or opportunity, statements that executives would be able to relate to and discuss. For example, one of the more important business goals for the Frequent Flyer program discussed earlier might be the following:

增加门票销售额5%。

Increase ticket sales by 5%.

这样的陈述有助于澄清项目的“原因”:你为什么要开发这个软件?但就像项目愿景一样,业务目标往往沟通不畅。许多开发团队对他们试图实现的业务目标只有一个模糊的概念。然而,了解这些目标对于在项目期间做出正确的范围、设计和实施选择至关重要。

A statement like this has the merit of clarifying the “why” of the project: why exactly are you building this software? But like the project vision, business goals are often very poorly communicated. Many development teams have only a vague notion of the business goals they’re trying to deliver. And yet, understanding these goals is essential to making correct scope, design, and implementation choices during the project.

您还可以使用以下“为了...正如...我想要...”格式来写目标:

You can also write goals using the following “In order to ... as ... I want to ...” format:

为了在未来一年内增加 5% 的门票销售额
作为 Flying High 销售经理
我想鼓励旅客选择 Flying High 而不是竞争对手的航班
In order to increase ticket sales by 5% over the next year
As the Flying High Sales Manager
I want to encourage travelers to fly with Flying High rather than with a rival company

这是许多项目团队在故事卡中使用的更传统的“作为... 我想要... 以便...”形式的变体。这种形式将目标放在首位(“为了...”),而传统的用户故事格式则将利益相关者放在首要位置。将目标放在首位强调了预期结果,这对业务来说应该是有明显价值的。当利益相关者放在首位时,最后一部分(“以便...”)往往会被掩盖,您可能会发现自己提出了高级功能,然后却难以用可靠的业务目标来证明它们。这是一种非常通用的格式,您将在整本书中使用它来描述各个抽象级别的需求。

This is a variation on the more traditional “As a ... I want ... so that ...” form used in many project teams for story cards. This form puts the goal first (“In order to ...”) as opposed to the more traditional User Story format, where the stakeholder is placed in the primary position. Putting the goal first places the emphasis on the expected outcome, which should be of obvious value to the business. When the stakeholder comes first, the last section (“so that ...”) tends to be eclipsed, and you may find yourself proposing high-level capabilities and then struggling to justify them with a solid business goal. This is a very versatile format, and you’ll use it to describe requirements at various levels of abstraction throughout the book.

无论你喜欢哪种格式,好的业务目标都应该是精确的。一些业务经理使用 SMART 缩写。业务目标应该是

Whatever format you prefer, a good business goal should be precise. Some business managers use the SMART acronym. Business goals should be

  • 具体的

  • Specific

  • 可衡量

  • Measurable

  • 可实现

  • Achievable

  • 相关的

  • Relevant

  • 有时限的

  • Time-bound

首先,目标应该明确地告诉读者你正在尝试做什么(“鼓励旅行者选择 Flying High”)。但它还应该描述你为什么要这样做,并概述你试图通过这样做实现的商业利益(“增加机票销售”)。

First and foremost, a goal should tell readers specifically what you’re trying to do (“encourage travelers to fly with Flying High”). But it should also describe why you want to do this and outline the business benefits you’re trying to achieve by doing so (“increase ticket sales”).

目标还应可衡量。可衡量的目标将让您更清楚地了解业务期望,并帮助您确定工作完成后目标是否已实现。您可以通过引入数量和时间概念使目标可衡量(例如,“明年将门票销售额提高 5%”)。

A goal should also be measurable. A measurable goal will give you a clearer idea of business expectations and also help you determine whether it has been achieved once the work is done. You can make a goal measurable by introducing notions of quantity and time (e.g., “Increase ticket sales by 5% over the next year”).

以这种方式量化目标还可以使确定解决问题的最佳方法或目标是否可以实现变得容易得多。在六个月内将销售额提高 200% 与在明年提高 5% 是完全不同的主张。了解时间范围(六个月)也使目标更加具体和集中。在所有这些情况下,精确的数字有助于管理期望并确保每个人都在同一页面上。

Quantifying a goal in this way will also make it much easier to determine how best to approach the problem, or whether the goal is even achievable in the first place. Increasing sales by 200% in six months would be quite a different proposition than increasing by 5% over the next year. Knowing the time frame (six months) also makes the goal more tangible and focused. In all of these cases, precise figures help manage expectations and ensure that everyone is on the same page.

但一个好目标最重要的属性可能是其相关性。如果一个目标在当前环境下能在规定的时间范围内对组织做出积极贡献,并且与组织的整体战略保持一致,那么这个目标就是相关的。相关目标是那些能对业务产生影响的目标。

But possibly the most important attribute of a good goal is its relevance. A goal is relevant if it will make a positive contribution to the organization in the current context within the specified time frame, and if it is aligned with the overall strategy of the organization. Relevant goals are goals that will make a difference to the business.

4.2.7 给我钱:业务目标和收入

4.2.7 Show me the money: Business goals and revenue

让我们仔细看看常旅客计划背后的商业目标。更完整的目标列表可能包括以下内容:

Let’s take a closer look at the business goals behind the Frequent Flyer program. A more complete list of goals might include the following:

  • 通过鼓励旅客选择 Flying High 而不是竞争对手的航班,在未来一年内将机票销售收入提高 5%。

  • Increase ticket sales revenue by 5% over the next year by encouraging travelers to fly with Flying High rather than with a rival company.

  • 通过树立飞行常客计划的积极形象,一年内将客户群增加 10%。

  • Increase the customer base by 10% within a year by building a positive image of the Frequent Flyer program.

  • 避免现有客户流失到新的竞争对手 Hot Shots 飞行常客计划。

  • Avoid losing existing customers to the new rival Hot Shots Frequent Flyer program.

  • 允许飞行常客计划会员直接在线使用积分购买机票,从而降低热线成本,而不像当前的计划那样,旅客需要拨打电话进行预订。

  • Reduce hotline costs by enabling Frequent Flyer members to purchase flights with their points directly online, unlike the current program, where travelers need to call to make a booking.

请注意,上述目标似乎都归结为通过增加收入或降低成本来赚取更多钱。从定义上讲,大多数商业组织的目标最终都是财务性质的。事实上,几乎所有商业目标都可以归为以下四类之一:

Notice that the preceding goals all seem to boil down to earning more money, either by increasing revenue or by reducing costs. The goals of most commercial organizations are, by definition, ultimately financial in nature. In fact, almost all business goals can be grouped into one of the four following categories:

  • 增加收入

  • Increasing revenue

  • 降低成本

  • Reducing costs

  • 保护收入

  • Protecting revenue

  • 避免未来成本

  • Avoiding future costs

例如,“增加 5% 的门票销售收入”(前面列出的第一个目标)显然是通过销售更多门票来增加收入。第二个目标“在一年内增加 10% 的客户群”与此类似,尽管与创收的联系略显间接。“避免失去现有客户”是保护收入的一个例子,而第四个目标“降低热线成本”显然是为了降低成本。

For example, “Increasing ticket sales revenue by 5%” (the first of the goals previously listed) is clearly about increasing revenue by selling more tickets. The second, “Increase the customer base by 10% within a year,” is similar, though the connection with generating revenue is slightly more indirect. “Avoid losing existing customers” is an example of protecting revenue, whereas the fourth, “Reduce hotline costs,” is clearly about reducing costs.

避免未来成本的一个例子可能是实施明年生效的新合规法规所要求的某些报告。在这种情况下,您现在投入精力是为了避免将来因不合规而被罚款;该功能本身不会增加任何直接的商业价值,但它可以避免未来的罚款成本。

An example of avoiding future costs might be implementing certain reports that will be required by new compliance legislation due to take effect next year. In this case, you’re investing effort now in order to avoid fines for noncompliance in the future; the feature itself doesn’t add any direct business value, but it avoids the future cost of fines.

公共服务组织的业务目标:新南威尔士州交通局

Business goals in a public service organization: Transport for NSW

新南威尔士州交通局是澳大利亚新南威尔士州负责公共交通的政府机构,该机构最近实施了一个项目,为谷歌地图提供火车和公共汽车的实时时刻表信息,就像世界上许多大城市所做的那样。旅行者不仅可以使用谷歌地图查找两地之间的公共交通选择,还可以根据来自公共汽车和火车的实时数据,了解下一班公共汽车或火车的到达时间。

Transport for NSW, the government agency responsible for public transport in New South Wales, Australia, recently implemented a project to provide Google Maps with real-time scheduling information for trains and buses, as is done in many large cities around the world. Travelers can use Google Maps not only to find public transport options between two locations, but also to find out when the next bus or train is scheduled to arrive, based on real-time data coming from the buses and trains.

这里没有盈利动机;目的是让公共交通乘客能够更有效地使用公共交通网络,减少乘客等待公共交通的时间。这背后的动机可能是提高客户满意度。更快乐的乘客更有可能使用这项服务而不是任何其他服务。次要目标可能是节省人们的时间,让他们有时间从事更有成效的工作,这反过来又有利于当地经济。该项目不会为实施该项目的政府机构带来任何增加的收入。

There’s no profit motivation here; the aim is to allow public transport passengers to be able to use the public transport network more efficiently and reduce the time passengers need to wait for public transport. The motivation behind this may be increasing customer satisfaction. Happier passengers are more likely to use this service instead of any alternatives. A secondary goal could be to save people time, freeing them up for more productive work, which in turn benefits the local economy. This project will generate no increased revenue for the government agency implementing the project.

这条规则仍然适用于政府或非营利组织,但形式略有修改。例如,公共服务对创造收入的兴趣不如对向公众提供有价值的服务的兴趣。这种类型的组织通常也有外部定义的预算;服务负责如何使用纳税人的钱,因此成本在业务目标中起着关键作用。一般来说,非营利组织项目的业务目标往往属于以下几种:类别:

This rule still holds true for government or nonprofit organizations, but in a slightly modified form. Public services, for example, are not as interested in generating revenue as they are in providing valuable services to the public. This type of organization also typically has an externally defined budget; the service is responsible for how taxpayer dollars are spent, so cost plays a critical role in business goals. In general, the business goals for projects in nonprofit organizations tend to fall into the following categories:

  • 改善服务

  • Improving service

  • 降低成本

  • Reducing costs

  • 避免未来成本

  • Avoiding future costs

4.2.8 揭开“为什么”的面纱:挖掘业务目标

4.2.8 Popping the “why stack”: Digging out the business goals

如果如果你想开发能够为客户带来真正价值的软件,那么了解项目的核心业务目标至关重要。对于一个实践 BDD 的团队来说,对业务目标、需求和关注点的深刻理解,以及整个团队的共识,尤为重要。有了对客户需求的深刻理解,团队就能更好地

If you want to build software that delivers real value to your customers, understanding the core business goals of a project is essential. And for a team practicing BDD, a deep understanding of business goals, needs, and concerns, shared across the whole team, is especially important. With a deep shared understanding of the customer needs, teams are better able to

  • 提出更广泛的可能实施方案

  • Propose a broader range of possible implementation solutions

  • 编写业务人员能够理解和关联的可执行规范,以便提供更好的反馈和沟通

  • Write executable specifications that business folk can understand and relate to, allowing better feedback and communication

  • 更有效地适应不断变化的情况;如果一个解决方案效果不佳或成本太高,他们可以尝试不同的方法,但仍然可以实现相同的业务目标

  • Adapt to changing circumstances more effectively; if one solution doesn’t work so well, or is too expensive, they can try out a different approach that will still deliver the same business goals

当团队成员尽可能多地参与需求发现过程而不是间接获取信息时,所有这一切都会变得容易得多。

All of this comes much easier when team members participate as much as possible in the requirements discovery process, rather than getting the information secondhand.

不幸的是,无论是业务发起人还是最终用户,通常都不会从纯粹的业务价值角度来表达他们的需求。相反,他们谈论的是他们心中的具体功能或解决方案。此外,利益相关者很少能够预先提供所有需求,即使他们事先知道这些需求。

Unfortunately, neither business sponsors nor end users typically express their needs in terms of pure business value. Instead, they talk in terms of concrete features or solutions that they have in mind. In addition, stakeholders are rarely capable of providing all the requirements up front, even if they do know them ahead of time.

目标驱动管理

Objective-driven management

不要告诉人们如何做事,告诉他们做什么,然后让他们的结果让你感到惊讶

Don’t tell people how to do things, tell them what to do and let them surprise you with their results.

        乔治·巴顿

        George S. Patton

   

   

乔治·巴顿将军是一位非常成功的美国将军,因其在第二次世界大战期间在欧洲战场取得的成就以及他非传统而充满活力的领导风格而闻名。巴顿曾在一次新闻发布会上被问及他如何能够让手下如此出色地发挥能力并忠诚不二。“我从不告诉人们该怎么做,”巴顿回答道。“我告诉他们做什么,但不告诉他们怎么做。如果你赋予人们责任,他们的聪明才智和可靠性会让你大吃一惊。”

General George S. Patton was a highly successful American general known for his achievements in the European theater during the Second World War and for his unconventional and dynamic leadership style. Patton was once asked at a press conference how he was able to get such outstanding competence and devoted loyalty from his staff. “I never tell people how to do things,” replied Patton. “I tell them what to do but not how. If you give people responsibility, they will surprise you with their ingenuity and reliability.”

大型组织的利益相关者通常被训练成在项目开始时就提供一份详细的所有需求清单。在许多项目中,一旦需求被批准,变更控制流程就会阻碍用户可能想要进行的任何修改、更正或添加。劝阻性的变更控制流程往往会导致需求集非常庞大,其中包含利益相关者可能想到的所有内容,许多功能被纳入其中只是因为它们“将来可能会派上用场”。如今,许多组织在引入敏捷实践时也继承了这种文化。

Stakeholders in large organizations have often been trained to believe that they must provide a detailed list of all their requirements at the start of a project. In many projects, once the requirements have been signed off on, change-control processes hinder any modifications, corrections, or additions that users might want to make. A dissuasive change-control process tends to result in very large sets of requirements full of everything the stakeholder can possibly think of, with many features being included just because they “might come in handy” someday. And this culture has been inherited by many organizations today even when they introduce agile practices.

很容易对这种流程产生的功能请求信以为真。毕竟,用户难道不应该比任何人都更清楚自己想要什么吗?为什么不直接记下他们想要什么,然后去做呢?有两个很好的理由说明这不是一个好主意。

It would be easy to take the feature requests that come out of such a process at face value. After all, shouldn’t the users know better than anyone else what they want? Why not just note down what they want, and then do it? There are two very good reasons why this would be a bad idea.

首先,提出这些需求的人(业务发起人、最终用户等)通常不精通用于提供解决方案的技术。他们要求的功能可能从技术角度(可能有更好的方法来完成工作)、从可用性角度(他们不是 UX 专家)或从财务角度(您可能能够使用其他方法更快、更便宜地完成工作)来看都不是最佳的。开发团队有专业责任为给定问题提出最合适的解决方案。但是,如果业务目标没有得到清晰的表达和理解,您将很难提出可能更合适的解决方案。

First, the people who produce these requirements—the business sponsors, the end users, and so forth—are typically not well versed in the technologies that will be used to deliver the solution. The features they request may not be optimal from a technical viewpoint (there may be better ways of getting the job done), from a usability perspective (they are not UX experts), or from a financial one (you may be able to get the job done faster and cheaper using a different approach). The development team has a professional responsibility to propose the most appropriate solution for a given problem. But if the business goals are not clearly expressed and understood, you’ll be hard-pressed to suggest potentially more appropriate solutions.

但还有第二个原因,可能比第一个原因更重要:如果你不知道为什么需要以特定方式交付某个功能,那么当发生变化时,你就无法知道这个功能是否仍然有用或相关。任何软件项目都是一个持续学习的过程。事物不可避免地会在此过程中发生变化,包括假设,甚至团队对某个特定功能如何使业务受益的理解。以详细技术解决方案形式表达的需求嵌入在假设和先入之见之中。你可以肯定,其中一些假设会被证明是不正确的,或者会在开发过程中发生变化,但很难预测哪些假设会是错误的。当发生变化时,你需要能够重新评估你正在构建的功能的相关性,但如果你不知道企业为什么需要某个功能,这将困难得多。

But there’s a second reason that’s arguably even more important than the first: if you don’t know why you need to deliver a feature in a particular way, you’ll have no way of knowing whether this feature is still useful or relevant when change happens. Any software project is an exercise in ongoing learning. Things inevitably change along the way, including assumptions and even the team’s understanding of how a particular feature might benefit the business. A requirement that’s expressed in the form of a detailed technical solution is embedded in a fabric of assumptions and preconceptions. You can be sure that some of these assumptions will turn out to be incorrect, or will change along the way, but it’s hard to predict which ones will be wrong. When changes do happen, you need to be able to reassess the relevance of the features that you’re building, but if you don’t know why the business wants a feature, this will be much more difficult.

通常,你需要深入挖掘才能发现企业真正需要什么。最好的方法之一就是反复问“为什么”,直到你找到可行的业务目标。根据经验,五个“为什么”式的问题通常足以确定潜在的业务价值(见图 4.4)。

You usually need to drill down a little to discover what the business really needs. One of the best ways to do this is to repeatedly ask “why” until you get to a viable business goal. As a rule of thumb, five why-style questions are usually enough to identify the underlying business value (see figure 4.4).

图 4.4 了解为什么要进行一个项目总是很重要的。

Figure 4.4 It’s always important to understand why you’re undertaking a project.

让我们来看一个例子。Sam 是 Fl​​ying High Airlines 的销售经理。他向业务分析师 Bianca 提出了一个要求:“我们需要将我们的飞行常客计划与主要信用卡供应商的联属计划整合在一起。”

Let’s look at an example. Sam is a sales manager with Flying High Airlines. He comes to Bianca, a business analyst, with a request: “We need to integrate our Frequent Flyer with an affiliate program with major credit card vendors.”

“当然可以,”比安卡说。“你能介绍一下这个功能吗?为什么我们的用户或利益相关者会看重这个功能?”

“Sure,” says Bianca. “Can you walk me through this feature? Why is this something our users or stakeholders would value?”

Sam 回答说:“我们与一家信用卡公司达成了协议,并向我们的会员提供特殊的 Flying High 信用卡,他们在消费时可以获得积分。我们只需要确保积分记入他们的账户即可。”

Sam replies: “We’ve made a deal with a credit card company and propose a special Flying High credit card to our members, and they earn points when they spend money. We just need to make sure the points get credited to their account.”

比安卡还没有确定价值来自何处,所以她问了另一个问题:“你能为我解释一下这将如何使我们的用户或利益相关者受益吗?”

Bianca hasn’t identified where the value is coming from to her satisfaction, so she asks another question: “Can you spell out for me how this will benefit our users or stakeholders?”

“我们的会员会很高兴,他们不仅在飞行时能得到优惠,在购物时也能得到优惠,”萨姆说。

“Well, our members will appreciate that they get benefits not just when they fly, but also when they shop,” says Sam.

此时,比安卡仍然不确定她是否已经确定了这项新功能背后的真正价值主张,因此她又问了另一个问题:“那么,如果我理解正确的话,我们想通过给予奖励和特权来增加飞行常客会员的数量,并且这有望增加机票销量?”

At this point, Bianca still isn’t sure she’s identified the real value proposition behind this new feature, so she follows up with another question: “So, if I understand correctly, we want to increase the number of Frequent Flyer members by giving them rewards and privileges, and this will hopefully increase the sale of tickets?”

Sam 解释道:“不完全是。我们批量向发行信用卡的银行出售飞行常客积分。银行希望人们用他们的卡消费,这样他们就可以从销售中赚取佣金。持卡人还可以获得其他福利,例如额外的托运行李,我们也向银行收取这笔费用。”

Sam explains: “Not exactly. We sell Frequent Flyer points in bulk to the banks that issue the credit cards. The banks want people to spend money with their cards, so they can earn a commission on the sales. Card holders also get other benefits, such as extra checked baggage, and we charge the bank for this too.”

“明白了,”比安卡说。“所以这里的主要商业利益来自飞行常客积分的销售,而不是机票销售。这项功能将如何帮助我们实现商业利益?”

“Gotcha,” says Bianca. “So the main business benefit here is from the sale of Frequent Flyer points, not from ticket sales. And how will this feature help us realize the business benefit?”

“我们已经与银行合作伙伴达成协议,但在我们的软件系统与他们的软件系统整合之前,我们不能真正发行卡或出售任何飞行常客积分。”

“We have negotiated a deal with our banking partner, but we can’t actually issue the cards or sell any Frequent Flyer points until our software system integrates with theirs.”

通过提出这些问题,比安卡现在清楚地找到了可衡量的业务目标:通过向银行出售飞行常客积分和其他会员福利来创造收入,银行再将这些福利转嫁给他们的客户。

By asking these questions, Bianca now has a clear mapping back to a measurable business goal: generate revenue from selling Frequent Flyer points and other member benefits to the banks, who then pass on these benefits to their customers.

这一过程有时被称为“弹出原因堆栈”,是一种强大的分析工具,BDD 团队经常使用它来更好地理解新功能请求。发现并明确用户要求的功能背后的价值主张,不仅可以帮助您了解他们要求这些功能的原因,还可以让您评估它们的相对价值并根据不断变化的情况进行调整。

This process, sometimes known as “popping the why stack,” is a powerful analysis tool, and BDD teams use it often to get a better understanding of a new feature request. Discovering and crystalizing the value proposition behind the features that users ask for not only helps you understand why they’re asking for these features but also puts you in a position to evaluate their relative value and to adapt them to changing circumstances.

BDD 是一个深度协作的过程。我们已经看到了确定和阐明业务愿景和目标的价值,这样整个团队就可以更深入地了解他们想要实现的目标。现在我们将研究如何确定可能有助于我们实现这些目标的功能和特性。我们将研究在 BDD 环境中特别有用的两种技术:影响映射和海盗画布。我们将在下一章中学习第三种技术,即功能映射。高级功能映射通常在推测阶段使用,尽管在说明阶段更常见。

BDD is a deeply collaborative process. We have seen the value of identifying and articulating business vision and goals so that the whole team can have a deeper understanding of what they are trying to achieve. Now we will look at how to identify the capabilities and features that might help us realize those goals. We will look at two techniques that are particularly useful in a BDD context: Impact Mapping and Pirate Canvases. We will learn about a third technique, Feature Mapping, in the next chapter. High-level Feature Mapping is often used during the Speculate phase, though it is more commonly encountered during the Illustrate phase.

4.3 影响图

4.3 Impact Mapping

影响图4是一种简单方便的方法,可用于初步了解您在项目中想要实现的目标。影响图直观、绘制速度快,业务人员和技术人员均可使用。它们有助于描绘出项目背后的业务目标、受项目影响的参与者以及使项目能够实现预期结果的功能之间的关系。它们还有助于记录这些目标并突出显示其背后的任何假设。

Impact Mapping4 is a simple and convenient approach to building up an initial picture of what you’re trying to achieve in a project. Impact maps are visual and intuitive, fast to draw, and accessible for both business and technical folk. They help create a picture of the relationship between the business goals behind a project, the actors who will be affected by the project, and the features that will enable the project to deliver the expected results. And they help document these goals and highlight any assumptions that underlie them.

这使得影响图成为启动初始需求分析过程和获得您要实现的目标的高级视图的绝佳方式。它们还可以帮助在项目进展过程中定义或调整项目里程碑。您可以在图 4.5 中看到我们的常旅客项目影响图的示例。但比地图本身更有趣的是我们如何创建它。

This makes Impact Maps a great way both to kick off the initial requirements-analysis process and to get a high-level view of what you’re trying to achieve. They can also help define or adjust project milestones as the project progresses. You can see an example of an Impact Map for our Frequent Flyer project in figure 4.5. But more interesting than the map itself is how we create it.

图 4.5 您可以做些什么来鼓励或允许利益相关者改变他们的行为以帮助实现业务目标?

Figure 4.5 What can you do to encourage or allow the stakeholders to modify their behavior to help achieve the business goal?

影响图是在利益相关者和开发团队成员之间的一次或一系列对话中构建的思维导图。与传统的需求分析方法不同,在传统方法中,团队会获得一份他们需要构建的预先设定的功能或用户故事列表,而影响图则首先确定业务目标和需要解决的问题。

An Impact Map is a mind map built during a conversation, or series of conversations, between stakeholders and members of the development team. Unlike traditional requirements analysis approaches, where teams are presented with a list of preestablished features or User Stories that they need to build, Impact Mapping starts by identifying the business goals and the problem that needs solving.

对话围绕五个不同但相关的问题展开:

The conversation centers around five different but related questions:

  • 痛点— 我们为什么要这样做?我们要解决什么业务问题?我们如何衡量它?

  • Pain point—Why are we doing this? What business problem are we solving, and how can we measure it?

  • 目标——我们要做什么?我们的目标是改进什么?改进多少?

  • Goal—What are we going to do about it? What do we aim to improve, and by how much?

  • 参与者——与我们的系统互动的关键人物是谁,他们的行为可以帮助或阻碍我们的目标?

  • Actor—Who are the key people who interact with our system, and whose behavior could help or hinder our goals?

  • 影响——我们如何帮助、鼓励或授权这些参与者帮助我们实现目标?我们希望看到哪些行为变化?

  • Impact—How can we help, encourage, or empower these actors to help us achieve our goals? What changes in behavior do we want to see?

  • 可交付成果— 哪些应用程序功能可能支持这些行为变化?作为交付团队,我们可以做些什么来帮助实现我们期望的影响?

  • Deliverables—What application features might support these changes in behavior? What can we do as a delivery team to help achieve the impact we are looking for?

让我们更详细地看一下这些问题。

Let’s look at each of these questions in more detail.

4.3.1 识别痛点

4.3.1 Identify the pain point

你需要问的第一个问题是,为什么要在首先。你正在解决什么问题?同样重要的是,你如何知道你是否已经有所作为?了解我们想要解决的痛点5有助于为我们在下一步中描述的业务目标提供背景和背景。

The first question you need to ask is why you’re building the software in the first place. What problem are you solving? And, just as importantly, how will you know if you have made a difference? Understanding the pain point5 we want to address helps to give context and background to the business goals we describe in the next step.

就我们航空公司而言,痛点可能是与竞争对手相比客户留存率较低。我们希望通过鼓励旅客再次乘坐我们的航班来提高销量。衡量这一痛点的一个好指标可能是与去年同期相比的机票总销量年。

In the case of our airline, the pain point might be low customer retention compared to competing airlines. We want to improve our sales by encouraging travelers to fly with us again. A good metric for this pain point might be the overall number of ticket sales compared to the same period in previous years.

4.3.2 定义业务目标

4.3.2 Define the business goal

一个业务目标描述了我们打算如何解决痛点。我们试图实现什么?我们认为它将如何改善情况,改善多少?每个影响图都以一个业务目标开始。例如,使用我们的常旅客示例,您可以从第一个目标开始,“通过鼓励旅客乘坐 Flying High 而不是竞争对手的航班,在未来一年内将机票销售收入提高 5%”。这将是您的影响图中的第一个节点,如图所示图 4.6。

A business goal describes how we intend to address the pain point. What are we trying to achieve? How do we think it will improve things, and by how much? Each Impact Map starts with a single business goal. For example, using our Frequent Flyer example, you might start with the first goal, “Increase ticket sales revenue by 5% over the next year by encouraging travelers to fly with Flying High rather than with a rival company.” This would be the first node in your Impact Map, as illustrated in figure 4.6.

图 4.6 影响图从您想要实现的业务目标开始。

Figure 4.6 An Impact Map starts with the business goal you want to achieve.

4.3.3 谁将受益?确定参与者

4.3.3 Who will benefit? Defining the actors

我们接下来要问的问题是。与我们的应用程序交互的参与者是谁?受项目影响的利益相关者是谁?谁将从结果中受益?如果你在销售某样东西,你的客户是谁?谁将能够导致或影响你想要实现的结果?谁可以阻止你的项目取得成功?

The next question we ask is who. Who are the actors who will interact with our application? Who are the stakeholders impacted by the project? Who will benefit from the outcomes? If you’re selling something, who are your customers? Who will be in a position to cause or influence the outcome you’re trying to achieve? Who can prevent your project from being a success?

正如您所见,所有项目最终都旨在以某种方式使组织受益。但组织是由人组成的,组织内的人(或与组织互动的人)将受到项目结果的影响。这些人是利益相关演员

As you’ve seen, all projects ultimately aim to benefit the organization in some way. But organizations are made up of people, and it will be people within this organization (or people who interact with this organization) who will be affected by the outcomes of your project. These people are the stakeholders or actors.

请注意,即使项目的整体效果旨在使组织受益,但项目对个人的影响也可能是负面的。例如,在抵押贷款申请过程中增加额外的步骤可能会让出售抵押贷款的银行家和申请房屋贷款的客户感到麻烦和烦恼。但降低不良贷款风险的净效应可能被认为对整个银行有利,足以抵消这些缺点。重要的是要意识到这些负面影响,并尽可能将其最小化。

Note that even when the overall effect of a project is designed to be beneficial for the organization, the impact of the project on an individual basis may be negative. For example, adding additional steps in a mortgage application process may be perceived as being cumbersome and annoying for the banker selling the mortgage and for the client applying for a house loan. But the net effect of reducing the risk of bad loans may be considered beneficial enough for the bank as a whole to outweigh these disadvantages. It’s important to be aware of these negative impacts and to try to minimize them where possible.

许多不同类型的利益相关者都会对您的工作成果感兴趣。您产品的未来用户可能是最明显的例子;他们的日常工作将受到您的项目的影响,无论是好是坏。对于飞行常客网站,这一类利益相关者将包括当前和未来的飞行常客会员。它还可能包括呼叫中心工作人员,他们将必须回答飞行常客会员关于新计划的问题,并能够查看客户的当前状态和里程。他们可能还需要执行飞行常客网站不支持的更复杂的任务,例如在家庭成员之间转移飞行常客里程或手动记入获得的里程。

Many different types of stakeholders will be interested in the outcomes of your work. Future users of your product are probably the most obvious example; their daily work will be impacted by your project, for better or for worse. For the Frequent Flyer website, this category of stakeholder will include current and future Frequent Flyer members. It may also include call center staff, who will have to answer questions from Frequent Flyer members about the new program and be able to view a client’s current status and miles. They may also need to perform more complicated tasks that are not supported on the Frequent Flyer website, such as transferring Frequent Flyer miles between family members or crediting earned miles manually.

请注意,未来的用户可能并不总是直接对业务目标感兴趣。常旅客会员没有特别的理由希望看到 Flying High 机票销售量增加。有时用户想要和要求的东西与最初的业务目标几乎没有关系。当您考虑需要提供哪些功能来实现业务目标时,您需要考虑如何鼓励这些利益相关者以有助于您实现目标的方式行事(见图 4.7)。例如,您希望提供一些功能来鼓励用户通过 Flying High 而不是竞争对手预订航班,以增加机票销售收入。

Note that future users may not always be directly interested in the business goals. A Frequent Flyer member has no particular reason to want to see an increase in Flying High ticket sales. Sometimes what the user wants and asks for will have little or no bearing on the original business objectives. When you think about what features you need to deliver to achieve the business goals, you need to think in terms of how you can encourage these stakeholders to behave in a way that helps you achieve the goals (see figure 4.7). For example, you want to provide features that encourage users to book flights with Flying High rather than with a competitor in order to increase ticket sales revenue.

图 4.7 浪费功能的示例。任何新功能的最终目的都是为组织带来价值。如果它没有以某种方式做到这一点,那么它可能就是浪费。

Figure 4.7 An example of a wasteful feature. The ultimate purpose of any new feature is to deliver value to the organization. If it’s not doing this in some way, it is probably waste.

另一类利益相关者包括那些自己不会使用应用程序,但会直接受到项目结果影响或对其感兴趣的人。例如,比尔是 Flying High Airlines 的销售总监,因此负责机票销售。机票销售收入增加 5% 将帮助他实现增加总体销售收入的目标。此外,他的预算用于支付新的常旅客会员网站的费用,因此了解和量化这一业务目标将使比尔能够以投资回报率 (ROI) 来证明项目成本的合理性。有了这些信息,比尔就能知道该项目可能增加多少收入,从而更清楚地知道他愿意花多少钱来实现这些增加的收入。

Another category of stakeholder includes people who won’t use the application themselves, but who are directly affected by, or interested in, the outcomes of the project. For example, Bill is the director of sales at Flying High Airlines and is therefore responsible for ticket sales. An increase in ticket sales revenue of 5% will help him meet his own goals of increasing sales revenue in general. In addition, his budget is paying for the new Frequent Flyer members’ website, so understanding and quantifying this business goal will give Bill a way to justify the cost of the project in terms of ROI (return on investment). With this information, Bill can see how much the project is likely to earn in increased revenue, and therefore have a clearer idea of how much he’s willing to spend to achieve these increased earnings.

其他利益相关者不会直接受到项目的影响,但他们希望对项目的实施有发言权,如果他们不满意,他们可能有权阻止该项目。监管者、安全人员和系统管理员就是这种角色的典型例子。

Other stakeholders will not be directly affected by the project but will want to have a say in how the project is implemented and may have the power to block the project if they aren’t contented. Regulators, those who work in security, and system administrators are good examples of this sort of role.

各利益相关者在实现业务目标和交付业务价值方面的实际作用经常被忽视。如果用户不以预期的方式使用应用程序,软件可能无法实现您预期的收益。从另一个角度来看,识别最有可能支持业务目标并提供真正价值的功能的一种非常有效的方法是从改变利益相关者行为的角度进行思考。您如何鼓励用户以支持业务目标的方式行事?

The actual role of the various stakeholders in achieving business goals and delivering business value is often overlooked. If users don’t use the application in the expected way, the software may not realize the benefits you expected. Looking at things from another angle, a very effective way to identify the features that are most likely to support the business goals and provide real value is to think in terms of changing stakeholder behavior. How can you encourage users to behave in a way that supports the business goals?

对于常旅客项目,您可以确定几个可能对“增加销售额”业务目标产生影响的重要利益相关者。为了简单起见,我们重点关注三个:首次旅行者、现有常旅客会员和呼叫中心员工。在影响图中,我们将这些参与者与业务目标联系起来,如您在图 4.8。

For the Frequent Flyer project, you could identify several significant stakeholders who might have an influence on the “increase sales” business goal. For the sake of simplicity, let’s focus on three: first-time travelers, existing Frequent Flyer members, and call center staff. In an Impact Map, we attach these actors to the business goal, as you can see in figure 4.8.

图 4.8 影响图的第二层调查谁将从业务目标中受益,或谁可以帮助实现业务目标。它还应包括任何可能阻止目标实现的人。

Figure 4.8 The second layer of an Impact Map investigates who will benefit from, or who can help achieve, the business goal. It should also include anyone who might be in a position to prevent this goal from happening.

4.3.4 他们的行为应该如何改变?定义影响

4.3.4 How should their behavior change? Defining the impacts

下一个要问的问题是如何。利益相关者和用户如何为实现业务目标做出贡献?他们的行为或活动如何改变才能更好地实现我们的业务目标并解决痛点?例如,如果他们是用户,你如何让他们更轻松地完成工作?如果他们是客户,我们如何鼓励他们选择我们的产品而不是竞争对手的产品?或者,他们或他们的行为如何可能阻止或妨碍这些目标的实现并导致项目失败?

The next question to ask is how. How can stakeholders and users contribute to achieving the business goals? How could their behavior or activities change to better meet our business goals and address the pain points? For example, if they are users, how could you make it easier for them to do their job? If they are customers, how can we encourage them to prefer our products to that of the competition? Alternatively, how might they or their behavior prevent or hinder these goals from being met and cause the project to fail?

在这里,您要考虑的是您希望对用户行为产生的影响。您正在考虑系统如何让利益相关者更容易为业务目标做出贡献,或者您如何影响他们的行为并以其他方式鼓励他们这样做。您是从利益相关者的角度来表达事物的,这强调了您正在尝试编写能够改变人们做事方式的软件,最好是以对业务有利的方式。

Here, you’re thinking about the impact you want to have on user behavior. You are considering how the system might make it easier for stakeholders to contribute to the business goals, or how you can influence their behavior and encourage them to do so in other ways. You’re expressing things from the point of view of the stakeholder, which emphasizes that you’re trying to write software that will change the way people do things, preferably in a positive way for the business.

就我们的常旅客计划而言,我们可以通过鼓励现有常旅客会员购买更多机票或吸引新客户加入该计划以便再次搭乘我们的航班来增加机票销售收入。我们可能会鼓励首次搭乘航班的乘客在机上注册我们的常旅客计划。对于新老会员,我们可以通过让他们觉得选择 Flying High 是正确的选择来增加他们搭乘我们航班的可能性。

In the case of our Frequent Flyer program, we could try to increase ticket sales revenue by encouraging existing Frequent Flyer members to buy more tickets or get new customers to join the program so that they might fly with us again. First-time flyers might be encouraged to sign up for our Frequent Flyer program while still onboard. For both new and existing members, we could increase the likelihood that they fly with us by making them feel that choosing Flying High is the right thing to do.

电话销售既耗时又费钱。减少电话售票时间也可以减少所需的呼叫中心员工数量,从而增加销售收入。您可以在影响图中说明这些概念,如图所示图 4.9。

Telephone sales are time-consuming and costly. Reducing the time taken to sell a ticket over the phone could also contribute to increasing sales revenue by reducing the number of call center staff required. You could illustrate these concepts in the Impact Map, as shown in figure 4.9.

图 4.9 这些利益相关者如何帮助实现这些目标?

Figure 4.9 How can these stakeholders help achieve these goals?

4.3.5 我们应该怎么做?定义可交付成果

4.3.5 What should we do about it? Defining the deliverables

您需要问的最后一类问题是什么。您的应用程序可以做什么来支持您在前三个阶段列出的效果?是否有其他方法可以在不使用软件的情况下实现这些结果?在构建应用程序的上下文中,“什么”对应于高级可交付成果。我们希望让用户能够做他们以前无法做到的事情,或者做一些他们可以更轻松地做的事情。这些功能通常(但并非总是)映射到软件功能。根据项目的规模,这些可能对应于史诗或高级功能,甚至整个计划计划。

The last type of question you need to ask is what. What can your application do to support the impacts you’ve listed in the previous three stages? Are there other ways to achieve these results without using software? In the context of building an application, the “what” corresponds to high-level deliverables. We want to give our users the capability to do something they couldn’t previously do, or to do something they could do more easily. These capabilities often, but not always, map to software features. Depending on the size of the project, these might correspond to epics or high-level features, or even whole program initiatives.

在我们的例子中,您可以通过让飞行常客会员更轻松地兑换航班或其他产品的里程,或允许他们在办理登机手续时使用积分升级航班,来鼓励飞行常客会员购买更多机票。

In our example, you might encourage Frequent Flyer members to buy more tickets by making it easier to redeem miles on flights or on other products or by allowing them to use their points to upgrade flights when they check in.

由于航空业对环境的影响,航空公司受到了许多指责。与竞争对手相比,您可以通过免费抵消其碳排放,或参与环保项目(如重新造林或植树),让旅客对乘坐 Flying High 航班感到更满意。您还可以通过确定哪些类型的预订是通过电话进行的,并简化这些机票的在线购买,从而减少电话预订的数量。

Airlines are receiving a lot of heat due to aviation’s environmental impact. You might make travelers feel better about flying with Flying High, compared to competing airlines, by offsetting their carbon emissions free of charge, or by engaging in environmentally friendly programs such as through reforestation or tree planting. And you might reduce the number of telephone bookings by determining what sort of bookings are being done over the phone and making online purchases for these tickets easier.

可交付成果也可以代表非软件计划或活动。例如,鼓励首次飞行的乘客注册常旅客计划的一种方法可能是建议他们刚完成的航班可获得双倍常旅客积分,条件是他们在 24 小时内注册。

Deliverables can also represent nonsoftware initiatives or activities. For example, one way to encourage first-time flyers to sign up with the Frequent Flyer program might be to propose double Frequent Flyer points on the flight they just completed, on the condition that they sign up within 24 hours.

影响影响映射可以帮助您直观地了解功能和交付成果如何有助于实现业务目标。它还有助于强调您可能做出的任何假设。例如,在这里,您假设社交媒体集成足以让常旅客会员自豪地发布他们的所有旅行。影响映射鼓励您不仅勾勒出这些关系和假设,而且还定义指标,以便在功能上线时使用它们来测试您的假设是否正确。在这种情况下,您可以跟踪常旅客会员生成的 Facebook 和 Twitter 帖子数量,以了解您的新社交媒体集成策略实际上有多有效。

Impact mapping helps you visualize how features and deliverables contribute to a business goal. It also helps underline any assumptions you may be making. For example, here you’re assuming that social media integration will be enough for Frequent Flyer members to proudly post about all their trips. Impact Mapping encourages you to not only sketch out these relationships and assumptions, but to also define metrics that you can use when the feature goes live to test whether your assumption was correct. In this case, you might keep track of the number of Facebook and Twitter posts that are generated by Frequent Flyer members in order to learn how effective your new social media integration strategy really is.

重要的是要记住,影响图不是计划;它们是可迭代的、动态的文档,可以帮助您直观地了解能力与业务目标之间的假设和关系。每当您将某样东西投入生产时,您都应该能够验证某些假设,并且您可能会证明其他假设是错误的。根据此反馈,您将能够相应地更新影响图和短期计划(见图 4.10)。

It’s important to remember that Impact Maps are not plans; they’re iterative, living documents that can help you visualize assumptions and relationships between capabilities and business goals. Whenever you deliver something into production, you should be able to validate certain assumptions, and you may prove others wrong. Based on this feedback, you’ll be able to update your Impact Map and your short-term plans accordingly (see figure 4.10).

图 4.10 哪些可交付成果可以帮助产生这种影响?

Figure 4.10 What deliverables can help make this impact?

4.3.6 逆向影响映射

4.3.6 Reverse Impact Mapping

影响在项目开始时,地图是极好的高级发现工具。但它们也可以非常有效地用于其他方向,尽管目的略有不同。例如,假设您已经有一组拟议的功能:敏捷项目中的产品待办事项、一组用例,甚至是更传统的需求规范文档中的一组高级需求。假设,此时请求的功能来自不同的利益相关者,他们每个人都确信“他们的”功能应该首先完成。

Impact Maps make great high-level discovery tools at the start of a project. But they can also be used very effectively in the other direction, though for a slightly different purpose. Suppose, for example, that you already have a set of proposed features: a product backlog in an Agile project, a set of use cases, or even a set of high-level requirements in a more traditional requirements-specification document. Suppose, while you’re at it, that the requested features come from different stakeholders, who are each convinced that “their” feature should be done first.

在这种情况下,影响图是一个很好的谈话开端。我发现以下策略很有效:召集利益相关者,将请求的功能写在白板上,确定他们将从中受益的人以及他们将如何从中受益,然后回到底层业务目标。最终,您将得到一个显示许多目标的图表,其中相对清楚地说明了哪些功能与哪些目标相对应。所有目标和支持功能的这种直观表示是讨论每个目标和每个功能的相对优点的绝佳起点,并且使更客观地对不同功能进行优先排序变得更加容易。

In this case, Impact Maps make a great conversation starter. I’ve found the following strategy effective: get your stakeholders together, put the requested features on a whiteboard, and identify who they’ll benefit and how they’ll benefit them, working back to the underlying business goals. Eventually you’ll end up with a graph that shows a number of goals, with a relatively clear illustration of which features map to which goals. This visual representation of all the goals and the supporting features is an excellent starting point for a discussion of the relative merit of each goal, and of each feature, and makes it much easier to prioritize the different features more objectively.

如果你有兴趣了解更多关于影响力地图的信息,请访问影响力地图网站 ( http://impactmapping.org )并阅读由戈伊科·阿兹克(《激发思考》,2012 年)。

If you’re interested in learning more about Impact Mapping, look at the Impact Mapping website (http://impactmapping.org) and read the book Impact Mapping by Gojko Adzic (Provoking Thoughts, 2012).

4.4 海盗画布

4.4 Pirate Canvases

海盗画布是高层利益相关者思考大局的另一种方式。海盗画布采用广度优先的方法进行产品设计,结合了影响地图中的元素、初创企业和营销领域的海盗指标以及 Goldratt 的约束理论。6

Pirate Canvases are another way for high-level stakeholders to think about the big picture. Pirate Canvases take a breadth-first approach to product design that combines elements from Impact Mapping, pirate metrics from the world of start-ups and marketing, and Goldratt’s theory of constraints.6

海盗画布鼓励的对话不只是关于显而易见的交付成果,还包括关于产品和服务的更广泛的生态系统。Peter Merel 7将此称为生态系统思维。它考虑我们如何激励生态系统中的各种参与者以互惠互利的方式行事,这不仅对我们自己有利,而且对他们也有利。这可以引发出意想不到的富有成果的讨论,揭示出以前未被认识到但紧迫的优先事项,甚至全新的商业机会。但在我们研究海盗画布之前,我们需要先研究海盗指标。

A Pirate Canvas encourages conversations not just about immediately obvious deliverables, but about the broader ecosystem of products and services. Peter Merel7 calls this Ecosystem Thinking. It considers how we might motivate various actors in the ecosystem to behave in a way that is mutually beneficial, that works not only for our own benefit, but for their benefit as well. And this can lead to surprisingly fruitful discussions that reveal previously unrecognized but urgent priorities and indeed whole new business opportunities. But before we can look at Pirate Canvases, we need to look at Pirate Metrics.

4.4.1 海盗指标

4.4.1 Pirate Metrics

后退2007 年,天使投资人戴夫·麦克卢尔 (Dave McClure) 确定了一般企业(尤其是初创企业)需要关注的五个关键指标,才能取得成功。这些指标是获取、激活、保留、推荐和回报,简称 AARRR(见图 4.11)。

Back in 2007, angel investor Dave McClure identified five key metrics that businesses in general, and start-ups in particular, need to focus on to be successful. The metrics in question are Acquisition, Activation, Retention, Referral, and Return, or AARRR (see figure 4.11).

图 4.11 您可以做些什么来鼓励或允许利益相关者改变他们的行为以帮助实现业务目标?

Figure 4.11 What can you do to encourage or allow the stakeholders to modify their behavior to help achieve the business goal?

获得指的是我们如何将参与者吸引到我们的产品和服务生态系统中。这通常涉及向潜在用户提供一些他们可以免费使用的细小但真实的好处,这将首先吸引他们使用我们的服务。

Acquisition refers to how we pull actors into our ecosystem of products and services. Often this will involve proposing some small but real benefit for potential users that they can use for free, which will attract them to our services in the first place.

但在大多数情况下,用户并不是我们生态系统中唯一的参与者。咖啡馆需要吸引咖啡爱好者的兴趣,也需要与咖啡供应商和咖啡师建立可持续的关系。银行需要考虑客户,也需要考虑投资者、金融服务提供商(银行可能会提供其产品)、会计软件提供商、监管机构等。

But in most cases users are not the only actors in our ecosystem. A cafe needs to grab the interest of coffee lovers, but also needs to build sustainable relationships with coffee suppliers and baristas. A bank needs to consider customers, but also investors, financial service providers (whose products the bank might supply), accounting software providers, regulators, and many others.

激活是让参与者参与我们的产品或服务。人们看到使用您的系统的价值。这可能是让他们尝试样品产品、开始免费试用、观看视频或注册时事通讯。当我们关注更广泛的生态系统而不是简单的销售渠道时,激活诱导用户表明身份,以便我们能够向某人(用户或其他行为者)收取其行为的费用。

Activation is getting actors to engage with our products or services. People see the value in using your system. This might be getting them to try a sample product, start a free trial, watch a video, or register for a newsletter. When we are looking at a broader ecosystem rather than a simple sales pipeline, activation is inducing users to identify themselves so that we can potentially bill someone (the users or some other actor) for their behavior.

保留就是我们如何让人们与我们建立关系。如何让用户回来获取更多信息?我们如何让他们留在我们的生态系统中?例如,我们网站有多少访客是回访者?他们多久会重新访问我们的网站一次?

Retention is all about how we get people to form a relationship with us. How do get users coming back for more? How do we get them to stay in our ecosystem? For example, how many visitors to our site are return visitors? How often do they revisit our site?

推荐是我们鼓励人们向我们介绍其他人的方式。这是推动增长的另一种重要且具有病毒式传播力的方式。我们的用户会向其他人推荐我们的服务吗?我们如何激励他们这样做?这可以采取积极的评论或意见的形式,也可以涉及更刻意的推荐朋友计划。

Referral is how we encourage people to introduce other people to us. This is another important, potentially viral, way to drive growth. Do our users recommend our services to others? How can we motivate them to do so? This could take the form of positive reviews or comments, or may involve a more deliberate refer-a-friend program.

最后,返回是关于我们如何让人们为我们带来切实的利益。每个用户为组织创造了多少价值?他们对组织目标的贡献有多大?我们可能会衡量销售组织的收入,或者其他方面,例如合规或监管政府部门的风险降低。我们甚至可能寻求衡量变革计划中的学习。

Finally, Return is about how we get people to deliver us with tangible benefits. How much value does each user generate for the organization? How much do they contribute to the organizational goals? We might measure revenue in a sales organization, or something else, such as risk reduction in a compliance or regulatory government department. We might even look to measure learning in a change initiative.

但为了有效,海盗指标应该与我们可以衡量的具体值相对应,并且对我们的特定领域有意义。通常,我们可以确定一些关键数字,我们可以使用它们来跟踪每个指标。了解这些数字是什么可以帮助我们知道我们需要做什么才能产生影响。

But to be effective, Pirate Metrics should correspond to concrete values that we can measure and that make sense for our particular domain. Very often, we can identify a small number of critical numbers that we can use to track each metric. Understanding what these numbers are can help us know what we need to do to make an impact.

例如,如果我们正在查看之前讨论过的飞行常客计划,我们可以使用以下内容:

For example, if we were looking at the Frequent Flyer program we discussed earlier, we could use the following:

  • 我们可以通过计算飞行常客网站的访客数量来衡量客户获取情况。

  • Acquisition could be measured by counting the number of visitors to our Frequent Flyer website.

  • 激活可能与注册飞行常客卡的用户数量相对应。

  • Activation might correspond to the number of users who sign up for the Frequent Flyer card.

  • 留存率可以衡量注册飞行常客计划并回来查看最新特价优惠的用户数量。

  • Retention might measure the number of users who sign up to the Frequent Flyer program and come back to check out the latest special deals.

  • 推荐可以跟踪社交媒体上的点赞或分享次数,或旅游网站上的正面评价次数。

  • Referral could keep track of the number of likes or shares on social media, or the number of positive reviews on travel websites.

  • 回报可以通过常旅客会员的机票销售量或者整体机票销售的影响来衡量。

  • Return could be measured by things like ticket sales from Frequent Flyer members, or maybe the impact in the overall ticket sales.

每项指标都衡量了业务增长道路上的一个制约因素或瓶颈。跟踪所有这些指标非常重要,这样您才能知道下一个需要解决的最重要问题在哪里。

Each metric measures a constraint, or bottleneck, on the road to a growing business. It is important to keep track of all of these metrics so that you can know where the next most important problem to solve lies.

例如,如果我们的用户找不到飞行常客网站,我们可能在获取阶段面临瓶颈。我们可以开展广泛的营销活动,让客户更多地了解飞行常客品牌。但如果我们的用户看不到我们产品的任何价值,也不会在网站上花费太多时间,我们可能面临激活瓶颈,在营销上投入更多资金将是浪费。或者,如果我们注意到用户在网站上花费时间,甚至注册飞行常客卡,但很少有用户返回网站,我们可能在留存方面存在问题。虽然所有这些指标都很有趣,但一般来说,我们应该尝试一次只优化其中的一个时间。

For example, if our users can’t find the Frequent Flyer website, we may be facing a bottleneck in the Acquisition phase. We could launch an extensive marketing campaign to make customers more aware of the Frequent Flyer brand. But if our users don’t see any value in our product, and don’t spend very much time on the site, we might have an Activation bottleneck, and spending more money on marketing will be wasteful. Alternatively, if we notice that users are spending time on the site and even registering for the Frequent Flyer card, but that very few users return to the site, we may have a problem with retention. While all of these metrics are interesting, in general we should try to focus on optimizing only one of them at a time.

4.4.2 从海盗指标到海盗画布

4.4.2 From Pirate Metrics to Pirate Canvases

海盗画布建立在海盗指标的概念之上,并对其进行了扩展。海盗画布不仅在现有产品或销售渠道的背景下看待这些指标,而且还将其作为一种更广泛地讨论生态系统发展制约因素的方式。

Pirate Canvases build on and extend the concept of Pirate Metrics. A Pirate Canvas looks at these metrics not just in the context of existing products or sales pipelines, but also as a way to discuss the constraints to the growth of an ecosystem more broadly.

对于现有产品和服务,海盗画布可以帮助我们组织有关如何消除瓶颈和改善结果的对话。但我们也可以使用这些指标来发现机会领域,我们可以在这些领域构建或整合新产品和服务,以填补以前未知的市场空白。海盗画布是一种很好的方式,可以突出我们对潜在新产品或服务的市场影响的假设。这反过来又有助于我们讨论如何测试这些假设,以及我们应该在哪些假设上努力第一的。

For existing products and services, a Pirate Canvas helps us structure our conversations about how we can remove bottlenecks and improve our results. But we can also use these metrics to discover areas of opportunity, where we might build or integrate new products and services to fill previously unknown market gaps. A Pirate Canvas is a great way to highlight our assumptions about the market impact of potential new products or services. And this in turn helps us discuss how we can test these assumptions, and which ones we should work on first.

4.4.3 发现糟糕的事情

4.4.3 Discovering what sucks

作为使用影响图,海盗画布从痛点开始。但海盗画布并不专注于特定的发布目标,而是采用广度优先的方法来确定最重要的目标。这可以帮助我们从许多不同领域发现并确定发布目标的优先级。海盗画布理念的创始人 Peter Merel 不谈论痛点。他喜欢问:“什么很糟糕?”

As with Impact Mapping, a Pirate Canvas starts with a pain point. But rather than focusing on a specific release goal, Pirate Canvases take a breadth-first approach to identifying the most important goals to work on. This can help us discover and prioritize release goals from a number of different areas. Peter Merel, originator of the Pirate Canvas idea, doesn’t talk about pain points. He likes to ask, “What sucks?”

让我们来看一个例子。Flying High 管理层注意到,他们在利润丰厚的商务旅行市场中的份额并不像他们希望的那么大。他们想看看如何才能增加他们在这个市场的收入份额。但要打入这个竞争激烈的市场,他们需要提供一些让 Flying High 有别于其他航空公司的东西。他们不仅想为商务旅客提供香槟和坚果,还想提供一些新的东西。问题是什么。

Let’s look at an example. Flying High management has noticed that their share of the lucrative business travel market is not as big as they would like. They would like to see how they can increase their revenue share from this market. But to break into this competitive market, they need to offer something that sets Flying High apart from other airlines. More than just offering champagne and nuts to business travelers, they want to offer something new. The question is what.

他们可以从非常高层次的问题开始,例如“乘坐 Flying High 商务旅行有什么不好?”但我们的敏捷教练 Carrie 更喜欢从更一般的问题开始:“商务旅行有什么不好?”更广泛的问题可以带来更广泛的思考和更具创新性的想法;它们可以帮助团队考虑他们可能能够提出哪些独特的解决方案。

They could start with a very high-level question such as “What sucks about business traveling with Flying High?” But Carrie, our Agile coach, prefers to start with something even more general: “What sucks about business travel?” Broader questions lead to broader thinking and more innovative ideas; they help teams consider what unique solution they might be able to bring to the table.

我们从五个海盗指标开始讨论。在上一节中,我们了解了这些指标如何应用于飞行常客计划。但海盗画布通常不只关注单个功能,甚至不关注应用程序,而是考虑公司运营所在的更广泛生态系统。我们鼓励参与者思考其产品和服务运营所在的整个生态系统中的问题和机遇。

We start this discussion by looking at the five Pirate Metrics. In the previous section, we saw how these metrics might apply to the Frequent Flyer program. But Pirate Canvases generally look beyond individual features or even applications and consider the broader ecosystem in which the company operates. Participants are encouraged to think about problems and opportunities across the whole ecosystem in which their products and services operate.

在海盗画布研讨会期间,参与者依次讨论每个指标,找出每个领域的问题,并使用我们之前看到的五个为什么技术来找到问题的根本原因。

During a Pirate Canvas workshop, participants discuss each of these metrics in turn, flushing out things that stink about each area, and using the five why’s technique we saw earlier to get to the root cause of the problem.

让我们看看 Flying High 团队是如何考虑商务旅客的糟糕之处,以及这会如何影响每个指标。Carrie 正在主持海盗画布会议。首先,她在白板上画了五列,分别对应五个海盗指标(见图 4.12)。“请记住,获取是将商务旅客纳入我们的生态系统,那么获取商务旅客有什么不好呢?”她问道。

Let’s watch as the Flying High team considers what stinks for business travelers, and how that might affect each of these metrics. Carrie is facilitating the Pirate Canvas session. First, she draws five columns on a whiteboard, corresponding to each of the five Pirate Metrics (see figure 4.12). “Remember, Acquisition is about bringing business travelers into our ecosystem, so what sucks about acquiring business travelers?” she asks.

图 4.12 海盗画布的上半部分标识了关键的瓶颈和障碍,并按海盗指标分组。

Figure 4.12 The top half of a Pirate Canvas identifies key bottlenecks and impediments, grouped by Pirate Metric.

销售部门的 Sam 对此有一些看法。“首先,人们对我们的商务产品没有太大兴趣。根据我们的分析,我们网站上只有 5% 的预订咨询来自商务用户。商务用户根本不会访问我们的网站。”

Sam, from sales, has some thoughts. “First, there just isn’t that much interest in our business offerings. According to our analytics, only 5% of the booking inquiries on our site come from business users. Business users just aren’t coming to our site.”

“那你认为这是为什么呢?”嘉莉问。

“And why do you think that is?” asks Carrie.

“嗯,他们没有特别的理由来我们的网站,”市场营销部门的马克说。“我们更出名的是作为一家针对旅游目的地的廉价航空公司。”

“Well, they have no particular reason to come to our site,” says Mark from marketing. “We are better known as a low-cost airline for tourist destinations.”

“嗯,商务旅客其实并不想去任何旅游网站,”Sam 指出。“说到底,他们甚至不想旅行。对于大多数商务旅客来说,旅行时间是一种浪费。”

“Well, business travelers don’t really want to go to any travel site,” points out Sam. “They don’t even want to travel, when it comes to it. Travel time is considered waste for most business travelers.”

“如果他们不想旅行,他们想要什么?”凯莉问道。

“If they don’t want to travel, what do they want?” asks Carrie.

“他们希望能做成交易。他们真正想要是有更多的时间和机会做生意,”萨姆回答道。

“They want to be able to do deals. What they would really like is more time and opportunities to do business,” Sam answers.

“啊哈!那么糟糕的是商务旅客不能在航班上做生意吗?”凯莉问道。

“Aha! So what sucks here is that business travelers can’t do business on their flights?” asks Carrie.

“确切地。”

“Exactly.”

Carrie 将这些要点记在便签上,并将它们贴在“获取”栏下的板上(见图 4.13)。“因此,当商务旅行者来到我们的网站时,我们就会获取他们,因为他们希望有机会做更多的生意,而不仅仅是旅行。但我们如何量化这一点?我们需要移动哪个刻度盘来改善情况?”

Carrie notes down these points on Post-Its and puts them on the board under the Acquisition column (see figure 4.13). “So we acquire a business traveler when they come to our site because they want the opportunity to do more business, not just to travel. But how could we quantify this? What dial do we need to move to improve things?”

图 4.13 Carrie 指出了获取阶段的主要障碍以及如何衡量进度。

Figure 4.13 Carrie notes the key impediments in the Acquisition phase and how to measure progress.

“最简单的衡量标准是衡量商务旅客的访问次数,”马克说。“我们可以从我们的网络分析中获得这些数据。”

“The simplest metric would be to measure the number of visits from business travelers,” says Mark. “We can get this data from our web analytics.”

接下来,Carrie 想谈谈激活。“在这种情况下,激活对我们来说意味着什么?请记住,这是客户产生‘啊哈’时刻并看到我们服务的价值的地方。”

Next, Carrie wants to talk about activation. “What does activation mean for us in this context? Remember, this is where the customer has an ‘aha’ moment and sees value in our services.”

马克说:“如果收购是为了吸引那些想要更多商机的商务旅客,那么激活就必须让他们愿意与我们一起飞行,以利用新的商机,无论这些商机是什么。”

“Well, if acquisition is about attracting business flyers who want more business opportunities, activation must be making them want to fly with us in order to take advantage of new business opportunities, whatever they might be,” says Mark.

Sam 继续说道:“就我们的情况而言,这可能与我们的商务旅客进行的富有成效的业务对话次数相对应。”

Sam continues: “In our case, that might correspond to the number of productive business conversations our business travelers are having.”

“那么收购有什么不好呢?”Carrie问道。

“And what sucks about acquisition?” asks Carrie.

“目前,在航班上做生意很难。假设我们已经解决了激活阶段,旅客可能知道航班上还有谁,但与除了你旁边的人以外的任何人交谈仍然是不切实际的,更不用说推销了,”马克指出。卡丽将这些要点记录在粉色卡片上,并将它们放在激活栏中(见图 4.14)。

“Currently, it is hard to do business on a flight. Assuming we have the Activation phase sorted, the travelers might know who else is on the flight, but it is still impractical to talk to anyone other than the person next to you, let alone make a sales pitch,” points out Mark. Carrie records these points on pink cards and puts them in the Activation column (see figure 4.14).

图 4.14 对于激活阶段,团队考虑为什么商务旅行者在飞行时无法开展业务。

Figure 4.14 For the Activation phase, the team considers why business travelers aren’t able to do business while flying.

接下来,Carrie 想谈谈留存问题。“留存就是从已获得的客户那里获得回头客。我们可以通过跟踪商务旅客的重复预订数量来轻松衡量这一点。但是什么会阻止我们的商务旅客再次光顾呢?”

Next Carrie wants to talk about retention. “Retention is getting repeat business from acquired customers. We can measure that easily by keeping track of the number of repeat bookings from our business flyers. But what would prevent our business travelers from coming back for more?”

“通常需要多次交谈才能达成销售。面对面交流总是比通过电子邮件或电话交流更好,”Sam 说道。“如果我无法轻松地与在飞行途中交谈过的人重新取得联系,那么这些飞行机会的价值就会大打折扣。”

“It usually takes more than one conversation to make a sale. And face-to-face is always better than by email or telephone,” says Sam. “If I can’t easily reconnect with people I talked to on the flight, it would really limit the value of these in-flight opportunities.”

“所以问题在于我们的商务旅客无法利用他们的航班来维持和发展他们的业务关系,”Carrie 继续说道。她在黑板上的“保留”栏中记录了这些想法。

“So the problem is that our business travelers can’t use their flights to leverage and maintain their business relationships,” Carrie resumes. She notes these ideas in the Retention column on the board.

“那么推荐呢?” Carrie 问道。“请记住,推荐是我们的客户为我们开展营销工作并鼓励其他人加入我们的生态系统的方式。”

“And what about referral?” asks Carrie. “Remember, referral is how our customers do our marketing work for us and encourage others to join our ecosystem.”

“就目前情况来看,商务旅客根本无法做到这一点,”马克指出。“他们没有动力邀请他们的商业伙伴乘坐我们的航班。”

“As it stands, there’s no way for business flyers to do this,” points out Mark. “They have no incentive to invite their business contacts to fly with us.”

“太好了,”卡丽说,“最后一个指标是回报。我们可以只衡量商业传单的销售收入。或者我们可以考虑得更广泛一些。如果我们换个角度看问题会怎样?如果我们能想出如何帮助我们的生态系统成长并从这种成长中受益会怎样?”

“Great,” says Carrie. “The last metric is return. We could just measure sales revenue from business flyers. Or we could think a bit more broadly. What if we looked at things differently? What if we could figure out how to help our ecosystem grow, and benefit from that growth?”

“这个想法不错,”迈克说。“很多小公司和初创公司如果不能派员工参加活动或拜访客户,可能会错失良机,但他们的差旅预算有限。也许我们可以采用某种初创企业加速器概念,初创公司给我们股权,以换取免费或打折的商务航班。我们可以为他们提供发展所需的旅行能力,并让他们与他们可能想做生意的其他旅行者一起乘飞机。”

“Good thinking,” says Mike. “There are plenty of small companies and start-ups that might miss out on opportunities if they can’t send their employees to events or customer visits, but their travel budget is limited. Maybe we could have some kind of start-up accelerator concept, where start-ups give us equity in exchange for free or discounted business flights. We could give them both the travel capabilities they need to grow and get them on flights with other travelers whom they might want to do business with.”

“好吧,我们先把这句话记为‘无法从传单交易中获利’,然后看看你的创业想法能给我们带来什么,”凯莉说。“我们可以通过追踪我们获得的股权的市场价值来衡量这一点。”

“OK, let’s note that as ‘No way to benefit from the deals flyers make,’ and see where your start-up idea takes us,” says Carrie. “We could measure this by tracking the market value of the equity we get.”

海盗画布现在看起来如图 4.15 所示。团队已经确定了他们面临的关键问题;现在是时候研究这些问题的一些可能的解决方案了问题。

The Pirate Canvas now looks like the one in figure 4.15. The team has identified the key problems they face; now it is time to look at some possible solutions to these problems.

图 4.15 海盗画布现在描述了每个阶段的主要障碍。

Figure 4.15 The Pirate Canvas now describes the key impediments at each stage.

4.4.4 打造史诗级景观

4.4.4 Building the Epic Landscape

海盗画布不仅仅旨在突出问题。更重要的是,它可以帮助团队更有效地讨论他们想要产生的影响,以及他们需要交付哪些高级可交付成果才能产生这种影响。这些高级可交付成果通常被称为史诗,我们将这种我们想要交付的史诗的广度优先愿景称为史诗景观——在我们试图解决的业务问题的背景下,我们需要交付的能力的广阔图景。

A Pirate Canvas does not just aim to highlight problems. More importantly, it helps teams have more effective conversations about the impact they want to make, and about what high-level deliverables they need to deliver to make this impact. These high-level deliverables are often referred to as Epics, and we call this breadth-first vision of the epics that we want to deliver an Epic Landscape—a broad picture of the capabilities we need to deliver, in the context of the business problems we are trying to solve.

为每个指标制定行动计划

Find an action plan for each metric

海盗画布研讨会的下一步是为每个海盗指标类别制定行动计划。团队制定痛点和指标,并将其转化为切实可衡量的目标。然后,从这些目标开始,他们遵循与影响图类似的方法来识别参与者、影响和可交付成果。这些可交付成果反过来成为史诗景观中的史诗。

The next step of a Pirate Canvas workshop is to come up with an action plan for each Pirate Metric category. The team makes the pain points and metrics, and converts them into tangible, measurable goals. Then, starting with these goals, they follow a similar approach to that used with Impact Mapping to identify actors, impacts, and deliverables. These deliverables in turn become the epics in the Epic Landscape.

正如我们在本章前面所看到的,影响图可以产生大量的功能和可交付成果,这些功能和可交付成果可以对任何给定目标做出贡献。海盗画布的做法略有不同。海盗画布是一种广度优先的方法,影响和可交付成果的森林将使人们更难专注于高影响的可交付成果。因此,虽然我们可能会发现许多潜在的影响和可交付成果,但我们会依次讨论每一个,只保留最有价值的。

As we saw in earlier in this chapter, Impact Mapping can produce a large number of capabilities and deliverables that could contribute to any given goal. Pirate Canvases do things a little differently. A Pirate Canvas is a breadth-first approach, and a forest of impacts and deliverables will make it harder to focus on the high-impact deliverables. So while we may discover many potential impacts and deliverables, we discuss each one in turn and only keep the most valuable ones.

让我们看看这个过程是如何运作的。Carrie 从“获取”栏开始。“让我们把获取的痛点变成目标。我们没有从商业客户那里获得足够的关注,我们想解决这个问题。我们需要增加多少网站访问量?你怎么知道你的解决方案是否有效?”

Let’s see this process in action. Carrie starts with the Acquisition column. “Let’s turn the acquisition pain point into a goal. We aren’t getting enough interest from business customers, and we want to fix this. How much of an increase in site visits do we need? How would you know if your fix worked?”

“目前,我们的游客很少,因此我们需要至少将商务旅客的访问量增加两倍才能产生影响,”萨姆说。

“We have very few visitors at the moment, so we would need to at least triple the visits from business travelers to make a difference,” says Sam.

Carrie 记下了这一点:

Carrie notes this down:

  • 原因— 增加 300% 的商务旅客访问量

  • Why—Increase site visits from business travelers by 300%

  • 对象— 商务旅客

  • Who—Business travelers

“我们需要采取什么措施来鼓励商务旅客使用我们的网站预订商务旅行?”

“And what actions do we need to take to encourage business travelers to use our site for booking their business trips?”

“我知道,”马克说。“我们需要的是一场大型的‘与我们一起飞行’营销活动。”

“I know,” says Mark. “What we need is a big ‘fly business with us’ marketing campaign.”

凯莉打断了他的话:“我们现在先不要担心什么。他们的行为应该如何改变?我们需要先确定这一点,然后才能担心我们应该怎么做才能实现这一点。”

Carrie cuts him short. “Let’s not worry about what just yet. How should their behavior change? We need that nailed down before we can worry about what we should do to make it happen.”

马克说:“我们希望他们改变行为,以便他们乘坐我们的商务航班来寻找商机。”

“We want them to change their behavior so that they come looking for business opportunities on our business flights,” says Mark.

“那我们该怎么做呢?”嘉莉问。

“And how could we do that?” asks Carrie.

“我们确实需要给他们一个访问我们网站的理由。免费给他们一些对他们来说很有价值的小东西,”Sam 回答道。

“We really need to give them a reason to come to our site. Give them some small thing for free, that is valuable to them,” replies Sam.

“如果我们能帮助他们提高旅行效率,那会怎样?”马克想。“例如,假设我们建立一个商务旅客在线社区,让他们知道哪些潜在客户或合作伙伴会乘坐他们的航班?”

“What if we could help them make their trips more productive?” wonders Mark. “For example, suppose we enabled an online community of business travelers who could know what sort of potential customers or partners would be flying on their flight?”

她在“获取”栏中垂直列出了这些要点,如图 4.16 所示。将它们放在板上可以让团队更容易地形象化和挑战他们所做的假设。这就是接下来发生的事情。

She lists these points vertically in the Acquisition column, as shown in figure 4.16. Putting them up on the board makes it easier for the team to visualize and challenge the assumptions they are making. This is what happens next.

图 4.16 海盗画布的底部描述了我们如何解决顶部的痛点。

Figure 4.16 The bottom part of a Pirate Canvas describes how we could address the pain points in the top section.

“你对如何做做出了很大的假设,”开发人员戴夫指出。“我们怎么知道商务旅行者真的想做这件事呢?”

“You’re making a pretty big assumption in the how,” points out Dave, the developer. “How do we know that is something business travelers would really want to do?”

“我们可以通过调查一些业务联系人轻松测试这一概念,”Sam 说。“但仔细想想,这并不是最冒险的假设。这听起来像是一种虚荣指标。网站访问量真的是衡量客户获取的最佳方式吗?衡量加入商务传单社区的人数不是更准确吗?”

“We could test the concept easily with a survey of some of our business contacts,” says Sam. “But come to think of it, that’s not the riskiest assumption. That why feels like a vanity metric. Are site visits really the best way to measure acquisition? Wouldn’t it be more accurate to measure the number of people who join the business flyers community?”

“当然,我们可以这样做,”卡丽说。“但我们还需要修改目标、参与者和影响,以确保它们仍然有效。”

“Sure, we can do that,” says Carrie. “But we will also need to revise the goal, actors, and impact, to make sure they are still good.”

“新的目标可能是在三个月内获得 1,000 份商业传单,”马克提出道。

“The new goal could be to acquire 1,000 business flyers within three months,” proposes Mark.

“有道理,”卡莉说。“在这种情况下,其他因素仍然成立。但我们最冒险的假设似乎是商务飞行将带来更多预订。我们如何测试这个假设?”

“Makes sense,” says Carrie. “And in that case, the other elements still hold. But our riskiest assumption seems to be that business flyers will result in more bookings. How can we test that assumption?”

“在我们为商务旅行者建立新的社交网络之前,让我们先测试一下我们的理论。我们为什么不在自己的销售人员身上试一试呢?”Sam 建议道。“我们可以提出一项内部服务,让一些愿意分享航班计划的高管分享他们的飞行计划。这样我们就可以知道人们是否愿意调整他们的旅行计划,以便与他们试图影响的高管共度时光。我们可以试验这个概念,而根本不需要编写任何软件。”

“Before we build a new social network for business travelers, let’s test our theory. Why don’t we try it out on our own sales staff?” suggests Sam. “We can propose an internal service where a few willing executives share their flight plans. That way we can see if people are willing to adjust their travel plans in order to get some time with executives they are trying to influence. We can trial the concept without having to write any software at all.”

“如果进展顺利的话,我们可以提出一个非常简单的商务传单计划,商务旅客可以注册查看其他旅客的个人资料并发送会议请求,”马克说。

“And if that goes well, we could propose a very simple Business Flyer program, where business travelers can sign up to see the profiles of other travelers and send meeting requests,” says Mark.

“目前在实际航班上开展业务还不太现实,但他们可以提前在休息室见面,”萨姆建议道。“这样我们就可以验证我们的假设,即更广泛的商务旅客会发现这是有用的。”

“Doing business on the actual flights won’t be very practical just yet, but they could meet up in the lounge beforehand,” proposes Sam. “This way we can test our assumption that this is something that business travelers more broadly would find useful.”

此类交付物具有切实的指标,突出了假设并清晰地表达了痛点,它们代表了团队现在可以讨论和实施的潜在发布目标。优先考虑。

Deliverables like this, with their tangible metrics, highlight assumptions and clearly articulate a pain point, and they represent a potential release goal that the team can now discuss and prioritize.

寻找其他指标

Finding the other metrics

在现实世界中,团队可能会认为一天就够了。他们有一个假设需要验证,还有一个实验需要运行。这个实验的反馈足以实现一个发布目标。不过,为了完整起见,让我们看看他们将如何继续讨论其他指标。

In a real-world scenario, the team might consider this enough for one day. They have a hypothesis to validate and an experiment to run. Feedback from this experiment would be enough for one release goal. For the sake of completeness, though, let’s see how they would continue their discussion about the other metrics.

“好的,那激活呢?”Carrie 说。“我们希望看到什么影响?”

“OK, what about activation?” says Carrie. “What impact would we like to see here?”

“一旦我们开始获得商业传单,我们希望他们参与该计划。我们希望看到他们与潜在客户或业务合作伙伴进行有益的对话,并建立新的联系,”马克说。“如果他们从该计划中获得了价值,他们就会回来购买更多。”

“Once we start to acquire business flyers, we want them to engage with the program. We want to see them having useful conversations with potential clients or business partners, and building new connections,” says Mark. “If they get value out of the program, they will come back for more.”

“我们来算一下这个数字吧,”嘉莉说。

“Let’s put a number on that,” says Carrie.

“我希望 30% 的商务预订能促成富有成效的对话,”Sam 说道。“我们可以在每次飞行后通过在线调查来衡量这一点。也许我们可以将其游戏化,让商务旅客在飞行后对对话进行评分,而当其他人对对话进行良好评分时,商务旅客会获得积分。”

“I would like to see 30% of business bookings lead to productive conversations,” says Sam. “We could measure that by an online survey after each flight. Maybe we could gamify it so that the business flyers rate the conversations after the flight, and business flyers get points when the other person rates the conversation well.”

“所以这是关于商务旅行者的,”卡丽说。“他们的行为应该如何改变?”

“So this is about the business travelers,” says Carrie. “How should their behavior change?”

“他们在旅途中进行了越来越多有用的对话,”马克说。我们可以通过很多方式来实现这一点。也许我们可以重新布置商务舱,这样更容易与不同的人交谈。我们可以在大厅发放名牌,甚至可以设立一些区域,让人们可以进行讨论或推销他们的产品或想法,有点像一个小型会议。”

“They have more and more useful conversations during their trip,” says Mark. We could do this in lots of ways. Maybe we can rearrange the business cabins so that it is easier to talk with different people. We could have name tags given out in the lobby, and maybe even have areas where people can hold discussions or pitch their product or idea, a bit like a mini conference.”

Carrie 记下了这一切(见图 4.17):

Carrie notes all this down (see figure 4.17):

  • 原因——30% 的航班预订会带来富有成效的对话。

  • Why—Thirty percent of flight bookings lead to productive conversations.

  • ——商业传单。

  • Who—Business flyers.

  • 如何——商业传单可以轻松地与潜在客户或商业伙伴进行对话。

  • How—Business flyers engage easily in conversations with potential clients or business partners.

  • 什么——适合交谈的商务舱、休息室内的小型会议。

  • What—Conversation-friendly business cabin, mini conferences in the lounge.

图 4.17 激活瓶颈集中在吸引商业传单上。

Figure 4.17 The activation bottleneck focuses on engaging business flyers.

请注意,这些交付物不一定涉及软件。海盗画布鼓励我们超越软件解决方案,思考如何改善更广泛的生态系统。

Notice how these deliverables don’t necessarily involve software. A Pirate Canvas encourages us to look beyond software solutions and at how we could improve the broader ecosystem.

留存方面确实涉及一些软件交付。“这都是为了吸引我们的商务旅客再次光临。我们的目标是,让 80% 的商务旅客重新预订,”马克说。

The retention side of things does involve some software deliverables. “This is all about enticing our business travelers to come back for more. As a goal, we want 80% of our business travelers rebooking,” says Mark.

“我们需要商务旅客跟进他们建立的联系,”Sam 说。“也许他们通过高评价对话获得的积分可以转化为飞行常客积分,他们可以用它来预订新的航班。”

“We need business travelers to follow up with the connections they make,” says Sam. “Maybe the points they earn for highly rated conversations can go toward Frequent Flyer points that they can spend on new flight bookings.”

“也许社交媒体应用程序可以帮助商务旅客与他们遇到的人建立关系并协调后续航班,”马克继续说道。

“And maybe a social media application could help the business flyers build relationships and coordinate subsequent flights with people they have met,” continues Mark.

嘉莉在黑板上记录了这些细节(见图 4.17):

Carrie records these details on the board (see figure 4.17):

  • 原因——80% 的新商务旅客在三个月内再次预订。

  • Why—Eighty percent of new business flyers book again within three months.

  • ——商业传单。

  • Who—Business flyers.

  • 如何——商务飞行员与他们的新联系人建立联系并为后续航班组织后续会议。

  • How—Business flyers connect with their new contacts and organize follow-up meetings for subsequent flights.

  • 什么——商业传单积分计划和社交媒体应用程序。

  • What—Business Flyer points program and social media app.

团队继续对另外两列进行同样的练习,为每个海盗指标提出一个或多个潜在可交付成果。他们发现了几个关键的可交付成果:

The team continues this exercise for the other two columns, coming up with one or more potential deliverables for each Pirate Metric. They discover several key deliverables:

  • 与其他社交媒体平台整合,以便商务传单可以邀请自己的联系人加入该计划

  • Integration with other social media platforms so that business flyers can invite their own contacts to join the program

  • 新的商务飞行者可以邀请通讯录中的联系人加入该计划,新联系人注册后,每个人都可以获得奖励飞行常客积分

  • A feature where new business flyers can invite contacts from their address book to the program, where each earn bonus Frequent Flyer points when the new contact signs up

您可以在图 4.18 中看到完成的海盗画布。

You can see the completed Pirate Canvas in figure 4.18.

图 4.18 完成的海盗画布展示了史诗般的景观,以及他们想要消除的瓶颈背景下的高级可交付成果。

Figure 4.18 A completed Pirate Canvas shows an Epic Landscape, high-level deliverables in the context of the bottlenecks they aim to eliminate.

海盗画布不限于每个指标一个参与者、一个影响或一个可交付成果。就像影响图一样,我们可以发现实现目标的许多途径。海盗画布的作用是绘制出我们可以处理的各种可能的可交付成果,以便我们了解它们如何有助于解决我们的痛点。将它们并排摆放在它们旨在支持的业务目标的背景下,是一种很好的方式,可以帮助团队确定优先事项并知道他们下一步应该在哪里投入精力。

A Pirate Canvas isn’t limited to one actor, one impact, or one deliverable per metric. Just like with Impact Mapping, we may discover many paths to delivering the goal we are after. The role of the Pirate Canvas is to map out the various possible deliverables that we could work on so that we can understand how they contribute to resolving our pain points. Laying them out side-by-side, in the context of the business goals they aim to support, is a great way to help teams prioritize and know where they should spend their efforts next.

海盗画布将画布的每个阶段视为增长道路上的潜在瓶颈或制约因素。这是一份动态文件:随着我们对瓶颈的理解不断发展,随着我们实施和提供消除制约因素的解决方案,动态将发生变化,优先事项将需要重新评估。

A Pirate Canvas views each stage of the canvas as a potential bottleneck or constraint on the path toward growth. It is a living document: as our understanding of the bottlenecks evolves, and as we implement and deliver solutions that remove constraints, the dynamics will change, and the priorities will need to be reevaluated.

概括

Summary

  • 战略需求发现是一个持续的过程,在整个项目中定期发生,并涉及高级利益相关者和交付团队成员。

  • Strategic requirements discovery is an ongoing process that happens regularly throughout the project and involves both senior stakeholders and delivery team members.

  • 识别和理解您构建的应用程序背后的基本业务目标,可以更轻松地设计和构建有价值的功能。

  • Identifying and understanding the fundamental business goals behind the applications you build makes it much easier to design and build valuable features.

  • 影响图使您能够直观地看到利益相关者、能力、特性和业务目标之间的关系。

  • Impact Mapping enables you to visualize the relationships between stakeholders, capabilities, features, and business goals.

  • Pirate Metrics 结合影响图并帮助利益相关者构建 Epic Landscape 的图景并了解他们应该首先关注哪些目标。

  • Pirate Metrics combine Impact Mapping and help stakeholders to build a picture of the Epic Landscape and to know which goals they should focus on first.

在下一章中,我们将研究如何更详细地描述功能以及如何将它们切分为用户故事。

In the next chapter, we look at how to describe features in more detail and how to slice them into User Stories.


1  Jeffrey L. Taylor,“假设驱动开发”,2011 年 1 月 13 日,https://www.drdobbs.com/architecture-and-design/hypothesis-driven-development/229000656

1  Jeffrey L. Taylor, “Hypothesis-Driven Development,” January 13, 2011, https://www.drdobbs.com/architecture-and-design/hypothesis-driven-development/229000656.

2  Panagiotis Mitkidis、Jesper Sørensen、Kristoffer L. Nielbo、Marc Andersen 和 Pierre Lienard,《集体目标归因增强人类合作》,PLoS ONE,第 8 卷,第 5 期,2013 年 5 月,http://www.plosone.org/article/info%3Adoi%2F10.1371%2Fjournal.pone.0064776

2  Panagiotis Mitkidis, Jesper Sørensen, Kristoffer L. Nielbo, Marc Andersen, and Pierre Lienard, “Collective-Goal Ascription Increases Cooperation in Humans,” PLoS ONE, vol. 8, no. 5, May 2013, http://www.plosone.org/article/info%3Adoi%2F10.1371%2Fjournal.pone.0064776.

3  Mitkidis 等人,“集体目标归因”。

3  Mitkidis et al., “Collective Goal Ascription.”

4  影响图是 Gojko Adzik 的创意,他在其同名著作(Provoking Thoughts,2012)中对其进行了描述。

4  Impact Mapping is the brainchild of Gojko Adzik, who describes it in his book of the same name (Provoking Thoughts, 2012).

5  影响图通常从业务目标开始。Daniel Schrader 在他的博客文章“业务成果影响图”(2017 年 11 月 17 日,https://elabor8.com.au/impact-mapping-for-business-outcomes)中提出了从痛点开始的想法,以帮助确定业务目标和相关指标。

5  Impact Mapping conventionally starts with the business goal. In his blog entry, “Impact Mapping for Business Outcomes,” November 17, 2017 (https://elabor8.com.au/impact-mapping-for-business-outcomes), Daniel Schrader proposes the idea of starting with the pain point to help identify the business goals and associated metrics.

6  Eliyahu M. Goldratt,《约束理论》(North River Press,1999 年)。

6  Eliyahu M. Goldratt, Theory of Constraints (North River Press, 1999).

7  Peter Merel 是海盗画布技术的创造者。

7  Peter Merel is the creator of the Pirate Canvas technique.

5 描述并确定特征的优先级

5 Describing and prioritizing features

本章封面

This chapter covers

  • BDD 和产品待办事项细化
  • BDD and Product Backlog refinement
  • 描述和组织特征
  • Describing and organizing features
  • 将功能分解为用户故事
  • Breaking features into User Stories
  • 使用实物期权来确定开发特定功能的最佳时机
  • Using Real Options to determine the best time to commit to building a particular feature
  • 利用深思熟虑的发现来减少未知因素的影响
  • Using Deliberate Discovery to reduce the impact of what you don’t know

令人惊讶的是,BDD 的许多好处都来自于与业务部门的简单对话,使用示例来挑战假设并建立对问题空间的共同理解。BDD 的主要好处之一就是鼓励和组织这种对话。在上一章中,您了解了了解为什么要开发软件以及从业务角度来看其最终目的是什么的重要性。我们研究了如何明确您想要实现的目标以及您希望这将如何使业务受益(业务目标),以及谁将受益或受到项目的影响(利益相关者)以及您需要在高层次上提供什么才能实现业务目标(能力)。我们还了解了影响图和海盗画布——两种强大的技术,可以帮助我们了解业务需求并确定可能满足这些需求的功能。

A surprising number of the benefits of BDD come from simply having a conversation with the business, using examples to challenge assumptions and build a common understanding of the problem space. One of the principal benefits of BDD is to encourage and structure this kind of conversation. In the previous chapter you learned how important it is to understand why you’re building a piece of software and what its ultimate purpose will be in business terms. We looked at how you can clarify what you want to achieve and how you expect this to benefit the business (the business goals), and also at who will benefit or be affected by the project (the stakeholders) and what you need to deliver at a high level to achieve the business goals (the capabilities). We also learned about Impact Mapping and Pirate Canvases—two powerful techniques that can help us understand business needs and identify features that might deliver these needs.

现在是时候描述如何提供这些功能了。在本章中,我们将讨论下一步,即讨论和描述您通过影响图或海盗画布发现的可交付成果。我们将学习如何将高级功能或史诗分解为用户故事,如何描述这些功能,以及如何使用深思熟虑的发现和真实选项对它们进行优先级排序,以及我们在此过程中学到的知识如何帮助我们规划即将到来的冲刺和发布。

Now it’s time to describe how you can provide these capabilities. In this chapter we look at the next step, which is to discuss and describe the deliverables you discovered through Impact Mapping or on a Pirate Canvas. We will learn how to break down high-level features or Epics into User Stories, how to describe these features, and how to prioritize them using Deliberate Discovery and Real Options, and how what we learn during this process can help us plan for upcoming sprints and releases.

我们将探讨的一些关键主题包括:

Some of the key topics we will explore include these:

  • 功能和用户故事。在 BDD 术语中,功能是帮助用户或其他利益相关者实现某些业务目标的软件功能。功能不是用户故事,但可以通过一个或多个用户故事来描述。用户故事是一种将功能分解为更易于管理的块的方式,使其更易于实现和交付。

  • Features and User Stories. In BDD terms, a feature is a piece of software functionality that helps users or other stakeholders achieve some business goal. A feature is not a User Story, but it can be described by one or several User Stories. A User Story is a way of breaking the feature down into more manageable chunks to make them easier to implement and deliver.

  • 管理不确定性在 BDD 实践中起着重要作用。当经验丰富的 BDD 从业者发现存在不确定性时,他们会避免过早地做出最终解决方案,而是保留各种选择,直到他们掌握了足够的信息,能够为手头的问题提供最合适的解决方案。这种方法被称为“真实选择”。

  • Managing uncertainty plays a major role in BDD practices. When they’ve identified areas of uncertainty, experienced BDD practitioners avoid committing to a definitive solution too early, keeping their options open until they know enough to be able to deliver the most appropriate solution for the problem at hand. This approach is known as Real Options.

  • 深思熟虑的发现试图通过尽可能主动地管理不确定性和无知来降低项目风险。

  • Deliberate Discovery tries to reduce project risk by managing uncertainty and ignorance proactively wherever possible.

但是我们首先来看一下 BDD 在传统敏捷活动产品待办事项细化中所扮演的角色。

But let’s start off by looking at the role that BDD plays in the traditional Agile activity of Product Backlog Refinement.

5.1 BDD 和产品待办事项细化

5.1 BDD and Product Backlog Refinement

我们在上一章中,我们了解了 BDD 团队如何在推测阶段使用海盗画布和影响图等技术来确定战略业务目标、能力和高级特性。推测阶段的另一个重要部分是详细说明产品待办事项中的特性,直到它们准备好由各个团队选择并在说明阶段进行改进(见图 5.1)。

We saw in the previous chapter how BDD teams identify strategic business goals, capabilities, and high-level features using techniques such as Pirate Canvases and Impact Mapping during the Speculate phase. Another important part of the Speculate phase is detailing features in the Product Backlog to the point that they are ready to be picked up by individual teams and refined during the Illustrate phase (see figure 5.1).

图 5.1 围绕敏捷需求的词汇有时会有点令人困惑。

Figure 5.1 The vocabulary around Agile requirements can be a little confusing at times.

这项活动通常称为产品待办事项细化,是一种常见的敏捷实践。该术语来自 Scrum。产品待办事项本质上是项目中需要完成的事情的列表,有点像团队的高级待办事项列表。对于实践 BDD 的团队,产品待办事项将包含我们在上一章中了解的发现活动中确定的功能,例如影响图和海盗画布。

This activity is often known as Product Backlog Refinement and is a common Agile practice. The term comes from Scrum. A Product Backlog is essentially the list of things that need to be done in a project, a bit like a high-level to-do list for the team. For a team practicing BDD, the Product Backlog will contain the features identified during the discovery activities we learned about in the previous chapter, such as Impact Mapping and Pirate Canvases.

根据 Scrum 指南,“产品待办事项细化是向产品待办事项中添加详细信息、估算和顺序的行为。这是一个持续的过程,产品负责人和开发团队在此过程中就产品待办事项的细节进行协作”(https://www.scrumguides.org)。

According to the Scrum guide, “Product Backlog Refinement is the act of adding detail, estimates, and order to items in the Product Backlog. This is an ongoing process in which the Product Owner and the Development Team collaborate on the details of Product Backlog items” (https://www.scrumguides.org).

在产品待办事项细化过程中,我们会对功能进行高层次评估,并确定关键示例和验收标准。较大的功能可能会被分解为较小的功能或故事,以便于讨论和规划,并讨论和定义即将发布的版本和迭代目标。

During Product Backlog Refinement, features are estimated at a high level, and key examples and acceptance criteria are identified. Larger features might be broken down into smaller features or stories to make them easier to discuss and plan, and upcoming release and iteration goals are discussed and defined.

产品待办事项细化活动在项目生命周期内定期进行:一些团队每周举行一次产品待办事项细化会议;其他团队在每个冲刺结束前不久举行一次常规会议。但其他产品待办事项细化活动(例如需求发现会议的研究和准备)也可以在较小的团队中进行。

Product Backlog Refinement activities happen on a regular basis during the life of a project: some teams have weekly Product Backlog Refinement sessions; others run a regular session shortly before the end of each sprint. But other Product Backlog Refinement activities, such as research and preparation for requirements discovery sessions, can also happen in smaller groups.

对于实践 BDD 的团队来说,产品待办事项列表中的项目是我们在第 4 章中讨论的战略发现会议中出现的高级特性或史诗。他们可能会使用高级特性映射(您将在第 6 章中了解)等技术来更好地理解正在讨论的特性。

For a team practicing BDD, items in the Product Backlog are the high-level features or Epics that emerged from the strategic discovery sessions that we discussed in chapter 4. They might use techniques such as high-level Feature Mapping (which you will learn about in chapter 6) to get a better understanding of the features being discussed.

产品待办事项细化活动并不总是涉及所有团队成员。在涉及多个团队的大型项目中,产品待办事项细化会议可以涉及各个团队的代表。

Product Backlog Refinement activities don’t always involve all team members. In larger projects involving multiple teams, Product Backlog Refinement sessions can involve delegates from the various teams.

在产品待办事项细化过程中,团队会将功能分解为用户故事。让我们更深入地了解这些术语的含义

During Product Backlog Refinement, a team takes features and breaks them down into User Stories. Let’s take a more in-depth look at what we mean by these terms.

5.2 什么是特征?

5.2 What is a feature?

敏捷项目中,开发人员使用许多不同的词语来描述他们想要构建的内容(见图 5.2):史诗​​、功能、主题、需求、特性、用例、用户故事、任务。令人困惑?罪名成立。敏捷社区在以清晰且普遍理解的方式定义这些概念方面做得相对较差。不同的方法和不同的团队使用不同的术语,有时甚至相互矛盾。尽管“用户故事”、“史诗”和“主题”等术语在敏捷和 Scrum 圈子中确实有着悠久的传统,但许多团队仍然浪费大量时间来争论他们应该使用什么术语,或者某个特定需求应该称为故事、特性、史诗还是其他完全不同的东西。

In Agile projects, developers use lots of different words to describe what they want to build (see figure 5.2): Epics, capabilities, themes, requirements, features, use cases, User Stories, tasks. Confusing? Guilty as charged. The Agile community has done a relatively poor job of defining these concepts in a clear and universally understood way. Different methodologies and different teams use varying—sometimes contradictory—terms. And although the terms “User Story,” “Epic,” and “theme,” for example, do have a long tradition in Agile and Scrum circles, many teams still waste long hours debating what terminology they should use or whether a particular requirement should be called a story, a feature, an Epic, or something else entirely.

图 5.2 围绕敏捷需求的词汇有时会有点令人困惑。

Figure 5.2 The vocabulary around Agile requirements can be a little confusing at times.

我们是如何陷入这种困境的?我们真正想做的就是描述我们认为用户需要什么。我们希望以一种业务利益相关者能够理解的方式来表达自己,以便他们能够验证我们的理解、做出贡献并尽早提供反馈。我们希望能够以一种更容易构建和交付满足这些需求的软件的方式来描述用户需要什么。

How did we get ourselves into this predicament? All we really want to do is describe what we think our users need. We want to express ourselves in a way that business stakeholders can understand so that they can validate our understanding, contribute, and provide feedback as early as possible. We want to be able to describe what our users need in a way that makes it easier to build and deliver software that meets these needs.

我们使用的术语旨在简化有关用户需求的讨论。本质上,我们试图做两件事:

The terms we use are intended to simplify discussion around user requirements. In essence, we’re trying to do two things:

  • 定期为企业提供有形、可见的价值

  • Deliver tangible, visible value to the business at regular intervals

  • 获得定期反馈,以便我们知道我们是否朝着正确的方向前进

  • Get regular feedback so we know if we’re going in the right direction

我们描述和组织需求的方式应该支持这些目标。不同团队组织和构建故事、史诗、功能等的各种方式只是将更高级别的需求分解为可管理的大小、以用户能够理解的术语描述它们并允许他们在每个级别提供反馈的方法。有很多完全合法的方法可以做到这一点,最适合您的团队的方法取决于项目的规模和复杂性以及组织和团队成员的背景和文化。

The way we describe and organize the requirements should support these goals. The various ways different teams organize and structure stories, Epics, features, and so forth are simply ways to decompose higher-level requirements into manageable sizes, describe them in terms that users can understand, and allow them to provide feedback at each level. There are many perfectly legitimate ways to do this, and what works best for your team will depend on the size and complexity of your project and on the background and culture of your organization and team members.

为了简单起见并保持一致性,让我们逐步介绍本书其余部分将使用的词汇。我们将主要讨论四个术语:功能特性用户故事示例(见图 5.3)。

For the sake of simplicity and consistency, let’s step through the vocabulary we’ll use throughout the rest of this book. We’ll mainly deal with four terms: capabilities, features, User Stories, and examples (see figure 5.3).

图 5.3 功能向利益相关者提供能力。我们将使用用户故事来规划如何交付功能。我们将使用示例来说明功能和用户故事。

Figure 5.3 Features deliver capabilities to stakeholders. We’ll use User Stories to plan how we’ll deliver a feature. We’ll use examples to illustrate features and User Stories.

我在第 3 章中简要介绍了这些概念,这里我们快速回顾一下:

I introduced these concepts briefly in chapter 3, but here’s a quick refresher:

  • 能力赋予用户或利益相关者实现某些业务目标或执行某些有用任务的能力。能力代表做某事的能力;它不依赖于特定的实现。例如,“预订航班的能力”就是一种能力。

  • Capabilities give users or stakeholders the ability to realize some business goal or perform some useful task. A capability represents the ability to do something; it doesn’t depend on a particular implementation. For example, “the ability to book a flight” is a capability.

  • 功能代表您为支持这些功能而构建的软件功能。“在线预订航班”就是一项功能。

  • Features represent software functionality that you build to support these capabilities. “Book a flight online” is a feature.

  • 当您构建和交付这些功能时,您可以使用用户故事将工作分解为更易于管理的部分并规划和组织您的工作。

  • When you build and deliver these features, you can use User Stories to break down the work into more manageable chunks and to plan and organize your work.

  • 您可以使用示例来了解这些功能将如何帮助您的用户并指导您在用户故事方面的工作。您可以使用示例来了解功能和单个用户故事。

  • You can use examples to understand how the features will help your users and to guide your work on the User Stories. You can use examples to understand both features and individual User Stories.

那么 Epics 怎么样?

What about Epics?

在 Scrum 和其他敏捷方法中,Epic通常是指无法在一次迭代中交付的大型用户故事。Epic 应该交付一项功能(尽管有时 Epic 可以贡献多种功能)。

In Scrum and other Agile methodologies, an Epic generally refers to a large User Story that can’t be delivered in a single iteration. An Epic should deliver a capability (though there may be times when an Epic can contribute to more than one capability).

功能还以某种方式提供或支持功能,并且可能需要多次迭代才能实现,因此术语上存在一些重叠。有些团队使用 Epic 一词来指代功能,就像我们在本章中所使用的一样,这很好。其他人认为 Epic 大于功能,因此 Epic 可能包含许多功能。这也可以。

A feature also delivers or supports a capability in some way, and may take more than one iteration to deliver, so there is some overlap in the terms. Some teams use the term Epic to refer to features in the sense that we are using them in this chapter, and this is fine. Others consider an Epic larger than a feature, so an Epic may contain many features. And this is fine too.

5.2.1 特性交付能力

5.2.1 Features deliver capabilities

作为开发人员,我们构建功能以便最终用户和利益相关者能够实现他们的目标。用户需要我们的软件为他们提供有助于他们实现这些业务目标的功能。

As developers, we build features so that end users, and stakeholders in general, can achieve their goals. Users need our software to give them the capabilities that will help them contribute to these business goals.

功能是我们为支持这些功能而向用户提供的。功能是一种有形的功能,对用户很有价值,并且与用户的实际要求密切相关,可能在一次迭代中交付,也可能无法交付。功能可以相对独立于其他功能交付,并由最终用户单独测试。功能通常用于计划和记录发布。

Features are what we deliver to users to support these capabilities. A feature is a tangible piece of functionality that will be valuable for users and that relates closely to what the users actually ask for, which may or may not be deliverable in a single iteration. A feature can be delivered relatively independently of other features and be tested by end users in isolation. Features are often used to plan and document releases.

功能以业务术语和管理层可以理解的语言来表达。如果您正在编写用户手册,则功能可能会有自己的章节或子章节。过去,当现成的软件装在盒子里时,功能就是包装侧面显示的内容。

Features are expressed in business terms and in a language that management can understand. If you were writing a user manual, a feature would probably have its own section or subsection. In the past, when off-the-shelf software was packaged in boxes, features were what appeared on the side of the package.

让我们来看一个例子。Flying High Airlines 对其最近推出的常旅客计划感到非常自豪。加入 Flying High 常旅客俱乐部可让会员获得积分,用于购买航班、升舱等,旨在鼓励旅客通过 Flying High 而不是竞争对手预订。但管理层注意到会员资格失效率很高,他们怀疑这是由于续订流程繁琐造成的。目前,会员需要致电 Flying High 或通过电子邮件寄回续订表格才能续订。

Let’s look at an example. Flying High Airlines is very proud of its recently introduced Frequent Flyer program. Belonging to the Flying High Frequent Flyer club lets members earn points that they can spend on flights, upgrades, and so forth, and it’s designed to encourage travelers to book with Flying High rather than a competitor. But management has noticed a high rate of lapsed memberships, which they suspect is due to the cumbersome renewal process. Currently, members need to call Flying High or return a renewal form by nonelectronic mail to renew.

假设您已确定了“使会员能够更轻松地续订会员资格”的功能。 Flying High 客户网站上支持此功能的一些有用功能可能是“允许会员在线续订会员资格”和“在会员资格到期时通知会员”。 为了支持这些功能,您可以定义诸如“在线续订会员资格”或“通过电子邮件通知会员其会员资格到期”之类的功能(见图 5.4)。

Suppose you have identified the capability “Enable members to renew their membership more easily.” Some useful features supporting this capability on the Flying High customer site might be “Allow members to renew their membership online” and “Notify members when their membership is due for renewal.” To support these capabilities, you could define features such as “Renew membership online” or “Notify members that their membership is due for renewal by email” (see figure 5.4).

图 5.4 如该影响图所示,功能为用户或利益相关者提供了做一些有用的事情的能力。

Figure 5.4 As illustrated in this Impact Map, features provide users or stakeholders with the capability to do something useful.

让我们重点介绍一下在线续订功能。您可以使用“为了...作为...我想要”格式来描述此功能,如下所示:

Let’s focus on the online renewal feature. You could describe this using the “in order to ... as a ... I want” format, like this:

功能:在线会员续订                  
为了更轻松地续订会员资格         
作为常旅客会员                          
我希望能够在线续订会员资格     
Feature: Online membership renewal                  
In order to renew my membership more easily         
As a Frequent Flyer member                          
I want to be able to renew my membership online     

此功能简介

A short summary of this feature

您支持什么能力或业务目标?

What capability or business goal are you supporting?

受益的主要利益相关者是谁?

Who is the main stakeholder who will benefit?

你想做什么?

What do you want to do?

您在上一章中使用了这种格式来描述功能,但它同样适用于任何级别的需求。您首先要简短地介绍或描述功能,以提供一些背景信息。这是图 5.4 中“如何”部分的影响图中提出的文本。事实上,在您实际安排、设计和实现此功能之前,这个简短的概述通常就足够了。只有当您相当确定想要(并且准备好)开始开发此功能时,您才真正需要充实细节。

You used this format in the previous chapter to describe capabilities, but it works equally well for requirements at any level. You start off with a short summary or title of the feature to give some context. This is the text proposed in the Impact Map in figure 5.4, in the “How” section. In fact, until you actually schedule, design, and implement this feature, this short overview is usually enough to work with. You only really need to flesh out the details when you’re fairly sure you want to (and are ready to) start working on the feature.

此时,您可以尝试更详细地阐述该功能的含义。首先,您要概述您认为该功能将支持的业务目标。这有助于提醒您为什么要首先构建此功能,并与影响图中的功能很好地联系起来。它还允许您对需求进行健全性检查。您可以问自己这样的问题:“我提议的功能真的能帮助我们实现这个业务目标吗?如果不是,它支持什么业务目标?根据我现在所知道的,是否仍然值得构建此功能?”自从您第一次设想该功能以来,您对需求的理解和项目背后的业务环境可能都发生了变化(见图 5.5),您可能需要重新评估该功能的真正重要性。

At that point, you can try to formulate what the feature is about in more detail. First you outline what business goal you think this feature will support. This helps remind you why you’re building this feature in the first place, and ties back nicely to the capability in the impact graph. It also allows you to do a sanity check on your requirement. You can ask yourself questions such as “Will the feature I’m proposing really help us achieve this business goal? If not, what business goal is it supporting? Based on what I know now, is it still worth building this feature?” Both your understanding of the requirements and the business context behind the project may have changed since you first envisaged the feature (see figure 5.5), and you might need to reevaluate how important the feature really is.

图 5.5 有时候,随着我们对特征了解的加深,它们变得不那么重要了。

Figure 5.5 Sometimes features become less important as we learn more about them.

接下来,您要确定哪些用户将受此功能影响,或者哪些利益相关者将受益。这有助于您从将使用该功能或​​预期从其结果中受益的人的角度来看待事物。

Next, you identify which users you think this feature will affect, or which stakeholders will benefit. This helps you look at things from the point of view of the people who will be using the feature or who expect to benefit from its outcomes.

最后,您要描述功能本身及其用途。在这里,您要重点描述软件在业务方面的作用;您还不想太纠结于技术细节或只专注于特定的实现。

Finally, you describe the feature itself and what it’s meant to do. Here, you focus on describing what the software does in business terms; you don’t want to get too hung up on the technical details or commit yourself to a particular implementation just yet.

有时,考虑需求时,最好不要从最终用户的角度,而要从最终对业务结果感兴趣的利益相关者的角度来考虑。例如,您可能会发现从业务的角度看待问题更有用:

Sometimes it’s more useful to consider a requirement not from the point of view of the end user, but from the perspective of the stakeholder who’s ultimately interested in this business outcome. For example, you might find it more useful to look at things from the point of view of the business:

功能:在线会员续订
为了减少会员流失造成的销售损失        
担任 Flying High 销售经理                                  
我希望会员能够在线续订会员资格    
Feature: Online membership renewal
In order to reduce lost sales from lapsing memberships        
As Flying High sales manager                                  
I want members to be able to renew their membership online    

这是企业的根本目标,而不是飞行常客会员的根本目标。

This is the underlying goal of the business, not of the Frequent Flyer members.

谁有兴趣实现这个目标?

Who is interested in obtaining this goal?

特征是指你想要影响其行为的参与者。

The feature refers to the actors whose behavior you want to influence.

这凸显了一个有趣的观点。真正希望会员续约的是 Flying High 销售经理,这样他们才能继续预订 Flying High 的航班。在这种情况下,您需要满足的真正利益相关者是销售经理,而不是会员。也许会员并不总是那么有续约的动力。也许您需要找到吸引他们续约的方法。

This highlights an interesting point. It’s really the Flying High sales manager who wants members to renew so that they’ll continue to book flights on Flying High planes. In this case, the real stakeholder you need to satisfy is the sales manager, not the members. Maybe the members aren’t always that motivated to renew. Maybe you need to find ways to entice them into renewing their memberships.

我们在这里使用的格式在 BDD 从业者中很受欢迎,因为它侧重于该功能旨在提供的业务价值或能力。但许多团队也使用更传统的“作为...我想要...以便”格式:

The format we’ve been using here is popular among BDD practitioners because it focuses on the business value or capability that the feature is meant to deliver. But many teams also use the more traditional “as a ... I want ... so that” format:

功能:在线会员续订
作为常旅客会员                          
我希望能够在线续订会员资格     
这样我可以更轻松地续订会员资格           
Feature: Online membership renewal
As a Frequent Flyer member                          
I want to be able to renew my membership online     
So that I renew my membership more easily           

受益的主要利益相关者是谁?

Who is the main stakeholder who will benefit?

他们想做什么?

What do they want to do?

您支持什么能力或业务目标?

What capability or business goal are you supporting?

如果您是新手,那么“为了...作为...我想要”格式将帮助您专注于业务目标,但这两种形式都有效,并且它们传达的信息基本相同。经验丰富的从业者将能够以这两种格式生成高质量且有意义的定义。

If you’re new to all this, the “in order to ... as a ... I want” format will help you stay focused on the business goals, but both forms are valid, and they convey essentially the same information. Experienced practitioners will be able to produce high-quality and meaningful definitions in both formats.

归根结底,描述一个特性没有正确或错误的方法,也没有必须使用的标准规范格式,不过在团队或团队内部达成一致的格式是很好的。项目。

Ultimately, there’s no right or wrong way to describe a feature, and no standard canonical format that you must use, though it’s nice to agree on a consistent format within a team or project.

练习 5.1参见图 5.6 中的影响图。您希望让呼叫中心员工能够通过电话更快速地售票。定义一些有助于支持此功能的功能。

Exercise 5.1 Look at the Impact Map in figure 5.6. You want to give call center staff the capability to sell tickets more quickly over the phone. Define some features that would help support this capability.

图 5.6 哪些功能可以支持此功能?

Figure 5.6 What features would support this capability?

5.2.2 功能可以分解为更易于管理的部分

5.2.2 Features can be broken down into more manageable chunks

当你描述一个特性时,你需要从功能的角度来思考,它能为最终用户提供一些有用的功能。当构建并交付一项功能时,您通常需要将功能分解为更小、更易于管理的部分。您可能能够也可能无法在一次迭代中交付整个功能。

When you describe a feature, you need to think in terms of functionality that delivers some useful capability to the end user. When you build and deliver a feature, you often need to break the feature down into smaller, more manageable pieces. You may or may not be able to deliver the whole feature in one iteration.

在探索交付特定功能的最佳方式时,您可以使用一种有效的功能分解形式进一步细分功能。在敏捷项目中,当您将功能分解为足够小的块以在一次迭代内构建时,您可以将这些块称为用户故事。如图 5.7 所示,通常需要多个级别的分解才能从现实世界的功能转变为合理大小的用户故事。

You can break features down further as you explore the best way to deliver a particular capability, using what’s effectively a form of functional decomposition. In an Agile project, when you’ve broken the features down into chunks small enough to build within a single iteration, you can call the chunks User Stories. As you can see in figure 5.7, it’s common to need more than one level of decomposition to get from a real-world feature to a reasonably sized User Story.

图 5.7 将功能分解为更小的功能或用户故事使得它们更易于组织和交付。

Figure 5.7 Breaking down features into smaller features or User Stories makes them easier to organize and deliver.

知道分解后的块何时不再是功能有点主观,并且因项目而异。正如我们前面所讨论的,功能是用户可以单独测试和使用的东西。功能本身可以提供商业价值;一旦功能完成,理论上你可以立即将其部署到生产中,而不必等待任何其他功能先完成。让我们看一些例子:

Knowing when the decomposed chunks are no longer features is a little subjective and varies from project to project. As we discussed earlier, a feature is something users can test and use in isolation. A feature can deliver business value in itself; once a feature is completed, you could theoretically deploy it into production immediately, without having to wait for any other features to be finished first. Let’s look at some examples:

  • “在线会员续订”当然可以算作一项功能。如果您将此功能单独部署到生产中,它仍然具有重大的商业价值。但是,您是否可以通过逐步交付此功能的较小部分而不是等待它完全完成来更快地提供商业价值?这通常是业务利益相关者的问题。

  • “Online membership renewal” would certainly qualify as a feature. If you were to deploy this feature into production by itself, it would still be of significant business value. But could you provide business value faster by incrementally delivering smaller parts of this feature rather than waiting for it to be completely finished? This is generally a question for the business stakeholders.

  • “通过信用卡续订会员资格”本身不算是一项功能,除非它包含整个续订流程。即使它包含,您仍然必须询问项目发起人是否愿意在不使用其他付款方式的情况下将此功能部署到生产中。

  • “Renew membership by credit card” would not be a feature in itself unless it included the entire renewal process. Even if it did, you’d still have to ask the project sponsor if they would be happy to deploy this feature into production without the other payment methods.

  • “使用 Visa 支付续费”和其他类似功能通常被认为级别太低,无法单独交付,特别是当它们只代表少量工作时。例如,使用 Visa 付款只是“使用信用卡续订会员资格”的一个方面,单独来看几乎没有商业价值,因此您应该以用户故事而不是功能的形式来表示它们。

  • “Pay renewal fees with Visa” and other similar features are often considered too low level to be delivered in isolation, especially if they represent only a small amount of work. Paying by Visa, for example, is just one aspect of “Renew membership by credit card,” and would be of little business value in isolation, so you’d represent these in the form of User Stories rather than features.

  • 另一方面,如果您在一家金融科技初创公司工作,并且接受每种不同类型的卡的付款需要大量工作,您可能会将它们视为一项独立的功能。在这种情况下,您可能会在第一个版本中推出“通过 PayPal 接受付款”,并在后续版本中推出“通过 Visa 接受付款”。

  • On the other hand, if you were working in a FinTech start-up, and accepting payments for each different type of card represented a large amount of work, you might see them as features in their own right. In that case, you might roll out “Accept payments via PayPal” in a first release and “Accept payments via Visa” in a subsequent release.

分解功能时有两种主要策略。这里使用的策略是将功能分解为多个较小的业务流程或任务(“信用卡续订”、“使用万事达卡付款”等)。您根据业务目标来表达任务,并尽量避免承诺特定的实施解决方案,直到您更了解哪种解决方案最合适。当您使用此策略时,诸如影响图之类的可视化方法也使您更容易从更大角度看待业务目标。这通常是实践 BDD(以及一般敏捷)时最有效的方法。

There are two main strategies when it comes to decomposing features. The one used here involves decomposing a feature into a number of smaller business processes or tasks (“Renew by credit card,” “Pay with MasterCard,” etc.). You express tasks in terms of business goals and try to avoid committing to a particular implementation solution until you know more about what solution would be most appropriate. When you use this strategy, visual approaches such as Impact Mapping also make it easier to keep the larger business goals in perspective. This is generally the approach that works best when practicing BDD (and Agile in general, for that matter).

团队有时使用的另一种策略是尽早决定需要构建什么,并提出用户故事来提供所设想的任何技术解决方案。这种方法有风险,需要更多的前期工作,并伴有危险。例如,图 5.8 显示了会员续订在线功能分解为多个用户故事的不同方式。

The other strategy that teams sometimes use is deciding what needs to be built early on and coming up with User Stories to deliver whatever technical solution is envisaged. This approach is risky and involves much more upfront work, with the dangers that entails. For example, figure 5.8 shows a different decomposition of the membership renewal online feature into a number of User Stories.

图 5.8 带着特定的解决方案来分解特征是危险的。

Figure 5.8 It’s dangerous to decompose features with a particular solution in mind.

在此分解中,您已经设想或设计了特定的屏幕序列来实现此功能,并已基于这些屏幕创建了用户故事。问题是,当您过早地致力于给定的解决方案时,您可能会失去对实际业务目标的关注,并且可能会错过提供更合适解决方案的机会。例如,在图 5.7 中,在实施一个基于 PayPal 的解决方案。

In this decomposition, you’ve already imagined or designed a particular sequence of screens to implement this feature and have created User Stories based on these screens. The problem is that you can lose focus on the real business goals when you commit early to a given solution, and you can miss the opportunity to provide a more appropriate solution. In figure 5.7, for example, the ability to renew memberships using Frequent Flyer points has been forgotten in all the excitement around implementing a PayPal-based solution.

5.2.3 一个功能可以通过一个或多个用户故事来描述

5.2.3 A feature can be described by one or more User Stories

用户故事是敏捷项目的核心,自敏捷诞生以来,故事就一直以略有不同的形式存在。用户故事是对用户或利益相关者想要实现的目标的简短描述,以企业可以理解的语言表达。例如,以下用户故事描述了一项要求,即在用户注册成为飞行常客会员时,强制用户输入至少一个中等复杂的密码。对于这个故事,您可以使用与之前功能非常相似的格式:

User stories are the bread and butter of Agile projects, and they’ve been around, in slightly differing forms, since the origins of Agile. A User Story is a short description of something a user or stakeholder would like to achieve, expressed in language that the business can understand. For example, the following User Story describes a requirement around forcing users to enter at least a moderately complex password when they register to be a Frequent Flyer member. For this story, you can use a format very similar to the ones used for features earlier on:

故事:注册时提供安全密码                  
为了避免黑客入侵会员账户               
作为系统管理员                                         
我希望新会员在注册时提供安全密码   
Story: Providing a secure password when registering                  
In order to avoid hackers compromising member accounts               
As the systems administrator                                         
I want new members to provide a secure password when they register   

这与您用于特征的格式相同。

This is the same format that you used for features.

用户故事传统上以故事卡的形式呈现,如图 5.9 所示,其中还包含其他详细信息,例如优先级和按照某些约定指标粗略估计的规模(估计通常以小时为单位,或者可能使用更抽象的“故事点”概念)。您还可以使用类似的卡片来表示功能。

User stories are traditionally represented on story cards like the one in figure 5.9, which also includes other details, such as a priority and a rough estimate of size in some agreed metric (estimates are often in hours, or they may use the more abstract notion of “story points”). You can also use similar cards to represent features.

图 5.9 典型的用户故事卡片格式

Figure 5.9 A typical User Story card format

在卡片的另一面,你可以用简单的要点列出一份初步的验收标准清单(见图 5.10)。这些验收标准明确了故事或功能的范围和界限。它们有助于消除歧义、澄清假设,并建立团队对故事或功能的共同理解。它们也是测试的起点。但这些验收标准的目的并不是明确或详尽无遗。在发现故事时,期望产品所有者或利益相关者想出一份明确的验收标准清单是不合理的。你只是想要足够的信息来继续前进。在实施故事时,甚至在你以后了解更多需求时,你将有足够的时间来完善、扩展和完成它们,并添加团队可能需要的任何其他需求文档。

On the flip side of the card, you can put an initial list of acceptance criteria in simple bullet points (see figure 5.10). These acceptance criteria clarify the scope and boundaries of the story or feature. They help remove ambiguities, clarify assumptions, and build up the team’s common understanding of the story or feature. They also act as a starting point for the tests. But the aim of these acceptance criteria isn’t to be definitive or exhaustive. It’s unreasonable to expect the product owner or stakeholders to think of a definitive list of the acceptance criteria when the stories are being discovered. You just want enough information to be able to move forward. You’ll have plenty of time to refine, expand, and complete them, and to add any additional requirements documentation that the team might need, when it comes to implementing the story, and even later on when you learn more about the requirements.

图 5.10 你可以在故事卡的背面放一份初步的验收标准清单。

Figure 5.10 You can put an initial list of acceptance criteria on the back of the story card.

如图 5.9 所示,这些用户故事看起来很像功能,但它们往往级别较低。用户故事不必单独交付,但可以专注于功能的某个特定方面。用户故事可以帮助您规划和组织如何构建功能。虽然您可能无法单独将用户故事投入生产,但您可以并且应该向最终用户和其他利益相关者展示已实施的故事,以确保您走在正确的轨道上,并了解有关实施后续故事的最佳方法的更多信息。

As you can see in figure 5.9, these User Stories look a lot like features, but they tend to be a little lower level. A User Story doesn’t have to be deliverable in isolation but can focus on one particular aspect of a feature. User stories can help you plan and organize how you’ll build a feature. Although you may not deliver a User Story into production by itself, you can and should show implemented stories to end users and other stakeholders to make sure that you’re on the right track and to learn more about the best way to implement the subsequent stories.

您可以使用用户故事来分解我们在上一节中讨论的功能。例如,图 5.11 建立在图 5.4 的基础上,继续调查哪些功能可能有助于您减少因飞行常客会员资格失效而造成的收入损失。

You can use User Stories to break down the features we discussed in the previous section. For example, figure 5.11 builds on figure 5.4, continuing the investigation of what features might help you reduce lost revenue from lapsed Frequent Flyer memberships.

图 5.11 您可以将大特征分解为更小、更易于管理的特征。

Figure 5.11 You can break large features down into smaller, more manageable ones.

您发现的满足此要求的功能之一是“会员资格失效的电子邮件通知”。这是一项相当大的工作,因此您可以将其分解为以下故事:

One of the features you discovered for this requirement was “Email notification of lapsing membership.” This is a fairly large piece of work, so you could break it down into stories like the following:

  • 向会员资格将在一个月内到期的会员发送通知电子邮件。

  • Send notification emails to members whose membership will finish within a month.

  • 配置通知消息文本。

  • Configure notification message texts.

  • 从通知电子邮件中打开续订页面。

  • Open a renewal page from the notification email.

虽然每个故事都以自己的方式增加了商业价值,但它们并非设计为独立部署到生产中。但它们确实为从利益相关者那里获得有用反馈提供了绝佳的机会。

Although each of these stories adds business value in its own way, they aren’t designed to be deployed into production independently. But they do provide great opportunities for getting useful feedback from stakeholders.

例如,假设您正在撰写以下故事:

For example, suppose you’re working on the following story:

故事:向会员资格即将到期的会员发送通知电子邮件 
一个月内
为了提高我们的常旅客计划的保留率
作为销售经理
我希望在会员资格到期前一个月通知会员
Story: Send notification emails to members whose membership will finish 
 within a month
In order to increase retention rates for our Frequent Flyer program
As a sales manager
I want members to be notified a month before their memberships finish

当你向 Flying High 销售经理展示该故事的实施情况时,对话大致如下:

When you show the implementation of this story to the Flying High sales manager, the conversation goes something like this:

您:电子邮件通知的工作原理如下。当他们的会员资格即将到期时,他们会收到一封如下所示的电子邮件。

You:      And this is how the email notification works. When their membership is about to expire, they receive an email that looks like this.

销售经理约翰:看起来不错。那么后续邮件怎么样?

John the sales manager: Looks good. And what about the follow-up email?

您:有后续电子邮件吗?

You:      Is there a follow-up email?

约翰:当然了。市场部的比尔想要一封后续邮件,其中包含某种折扣优惠,以鼓励前会员回来。

John:     Of course. Bill from marketing wants a follow-up email that will include a discount offer of some kind to encourage ex-members to come back.

您:折扣总是一样吗?

You:      And is the discount always the same?

约翰:不,比尔需要能够根据他最新的营销策略进行更改。我们上次谈到了配置消息。

John:     No, Bill needs to be able to change it depending on his latest marketing strategy. We talked about configuring the messages last time.

这种反馈的价值是巨大的。您刚刚发现了一个关于之前被忽视或最初被误解的要求的新故事:“向会员资格刚刚失效的前会员发送后续通知电子邮件。”

The value of this sort of feedback is huge. You’ve just discovered a new story for a requirement that had been overlooked or initially misunderstood: “Send follow-up notification emails to ex-members whose membership has just lapsed.”

此外,您现在对“配置通知消息文本”故事的期望有了更清晰的了解,我们接下来将讨论该故事。最初,这被认为是一个可配置的模板,开发团队可以在需要时更改并在下一个版本中交付。但根据这次对话,您现在知道营销人员希望能够随时配置消息。您现在可以按如下方式描述下一个故事:

In addition, you now have a clearer understanding of what’s expected regarding the “Configure notification message texts” story, which we’ll look at next. Originally this was conceived of as a configurable template that the development team could change when required and deliver in the next release. But based on this conversation, you now know that the marketing people want to be able to configure the message at any time. You can now describe this next story as follows:

故事:在后续通知中包含可配置的激励措施
为了提高我们的常旅客计划的保留率
作为销售经理
我希望能够包含一个可配置的文本,描述激励措施 
重新加入,如通知中的折扣优惠或奖励积分 
留言
Story: Include a configurable incentive in the follow-up notifications
In order to increase retention rates for our Frequent Flyer program
As a sales manager
I want to be able to include a configurable text describing incentives to 
 rejoin such as discount offers or bonus points in the notification 
 message

此示例说明了另一点。用户故事允许您将定义详细需求推迟到尽可能晚的时候。随着时间的推移,您将越来越了解您正在交付的系统。这就是 BDD 所促进的持续对话旨在促进的。如果您过早指定用户故事的细节,您可能会错过一些稍后会了解的重要事实。如果您首先指定“配置通知消息文本”故事的细节,那么您将实现一项几乎没有商业价值的功能,并且它根本不符合利益相关者的期望。

This example illustrates another point. User stories allow you to put off defining detailed requirements until as late as possible. As time goes on, you’ll learn more and more about the system you’re delivering. This is what the ongoing conversations promoted by BDD are designed to facilitate. If you specify the details of a User Story too early, you may miss some important fact that you’ll learn later on. If you specified the details of the “Configure notification message texts” story first, you’d implement a piece of functionality with little business value, and it wouldn’t correspond to the stakeholders’ expectations at all.

但你不能永远拖延下去。如果你太晚解决它,你将没有时间在功能到期前与利益相关者交谈并了解详细需求。这就是我们所说的最后的责任时刻。这一概念在实物期权方法中也有广泛应用,我们将在本文后面详细讨论。章。

But you can’t procrastinate forever. If you address it too late, you won’t have time to talk to stakeholders and understand the detailed requirements before the feature is due. This is what we call the last responsible moment. This concept is also heavily used in an approach called Real Options, which we’ll look at in more detail later in this chapter.

练习 5.2开发您在上一个练习中定义的功能,并将它们分解为不同大小的故事,直到您得到您认为可管理的故事大小。使用“为了...作为...我想要”格式更详细地描述其中一些故事。

Exercise 5.2 Develop the features you defined in the previous exercise and break them down into stories of different sizes until you get to stories that you think are of a manageable size. Describe some of them in more detail using the “in order to ... as a ... I want” format.

5.2.4 功能不是用户故事

5.2.4 A feature is not a User Story

在许多项目中,我们讨论的功能将以高级用户故事的形式呈现,而有些团队认为没有必要将功能分解为较小的故事。这很好,并且适用于较小的项目。

In many projects, the features we’ve been discussing would be represented as high-level User Stories, and some teams don’t find it necessary to break the features down into smaller stories. This is fine and will work well on smaller projects.

但保持两者之间的区别也有一些好处。记住,

But there are some advantages to keeping a distinction between the two. Remember,

  • 功能是您向最终用户或其他利益相关者提供的功能,以支持他们实现业务目标所需的能力。

  • A feature is a piece of functionality that you deliver to end users or to other stakeholders to support a capability that they need in order to achieve their business goals.

  • 用户故事是一种规划工具,可以帮助您充实特定功能所需提供的细节。

  • A User Story is a planning tool that helps you flesh out the details of what you need to deliver for a particular feature.

您可以提前相当长一段时间定义功能,但您希望在说明阶段开始为功能创建故事,这通常发生在新冲刺开始之前或开始实施功能的工作之前。

You can define features quite a bit ahead of time, but you want to start creating stories for a feature in the Illustrate phase, which generally happens shortly before a new sprint starts or before work starts on implementing a feature.

重要的是要记住,用户故事本质上是规划工件。它们是组织交付功能所需工作的绝佳方式,但最终用户并不关心您如何组织事物以推出该功能,只要它能够交付即可。未来的开发人员对应用程序当前的功能比您如何构建它更感兴趣。

It’s important to remember that User Stories are essentially planning artifacts. They’re a great way to organize the work you need to do to deliver a feature, but the end user doesn’t really care how you organize things to get the feature out the door, as long as it gets delivered. Future developers are more interested in what the application currently does than how you went about building it.

一旦功能实现,就可以丢弃用户故事。功能描述通常更能有效地描述应用程序的功能。您用来说明功能的示例和规则可以很好地说明软件的实际工作原理,您稍后将在本章中编写的自动验收标准也是如此书。

Once the feature has been implemented, the User Stories can be discarded. The description of the features is generally more effective at describing what the application does. The examples and rules you use to illustrate the features do a great job of illustrating how the software actually works, as do the automated acceptance criteria that you’ll write later on in this book.

5.2.5 发布特性和产品特性

5.2.5 Release features and product features

作为我们已经看到,功能是允许用户实现某些业务目标或帮助他们更轻松地实现目标的东西。从 BDD 的角度来看,特性是支持功能的功能。我们可以将其称为产品特性。如果您在软件装在盒子里的时候构建软件,那么这样的特性就足够重要,可以出现在产品包装盒的侧面。

As we have seen, a capability is something that allows users to achieve some business goal or that helps them to achieve a goal more easily. And from a BDD perspective, a feature is a piece of functionality that supports a capability. We might call this a product feature. If you were building software back when software came in boxes, such a feature would be important enough to appear on the side of the product box.

但是在许多规模化敏捷方法论中,例如 XSCALE ( https://xscalealliance.org )、LeSS ( https://less.works ) 和 SAFe ( https://www.scaledagileframework.com ),“功能”一词的用法略有不同。在这些框架中,功能描述的是我们要发布的新功能或修改功能,而不是绝对的系统行为。我们将其称为(为了清楚起见)发布功能。发布功能是启用或支持某些业务目标并可用于发布计划的功能。事实上,它是您可以单独交付的最小功能片段,同时仍能显示一些切实的业务利益。发布功能不一定小到可以在一次迭代中构建和交付,因此它们通常被切分为用户故事,以使其更易于管理。思考发布功能的一个好方法是,它可能会出现在发布说明中的项目符号列表中。

But in many scaled Agile methodologies such as XSCALE (https://xscalealliance.org), LeSS (https://less.works), and SAFe (https://www.scaledagileframework.com), the word “feature” is used slightly differently. In these frameworks, features describe new or modified functionality that we want to release, not system behavior in absolute terms. Let’s call these (for the sake of clarity) Release Features. A release feature is a piece of functionality that enables or supports some business goal and that can be used for release planning. In fact, it is the smallest slice of functionality that you can deliver in isolation and still show some tangible business benefit. Release features are not necessarily small enough to be built and delivered in a single iteration, so they are often sliced into User Stories to make them easier to manage. A good way to think of a release feature is that it might appear in a bullet-point list in your release notes.

用法上的差异很微妙,但必须注意,因为它对我们组织和维护 BDD 可执行规范和动态文档的方式有影响。随着新版本功能的推出,产品功能可能会在产品的整个生命周期内得到改进。事实上,发布功能可以描述对现有产品功能的改进或全新的功能。

The difference of usage is subtle but important to be aware of, as it has implications for the way we organize and maintain our BDD-executable specifications and living documentation. A product feature may improve over the life of the product, as new release features are delivered. In fact, a release feature can describe an improvement to an existing product feature or an entirely new one.

让我们来看一个现实世界中的例子来说明这种区别。假设您在一家金融科技初创公司工作,试图为企业客户开发一款新的网上银行应用程序。客户希望能够做的一件事就是设置直接借记。您可以这样表达潜在的业务目标:“我们相信,如果客户可以使用我们的应用程序通过直接借记支付账单,他们将更有可能在我们这里开户,而不是在没有此功能的竞争对手那里开户。”

Let’s look at a real-world example of this distinction. Suppose that you are working for a FinTech startup, trying to develop a new online banking application for business customers. One thing customers want to be able to do is to set up direct debits. You might express the underlying business goal like this: “We believe that if clients can pay their bills via direct debit with our app, they will be more likely to open an account with us rather than with our competitors who do not have this capability.”

直接借记是您希望添加到产品中的一项功能。您可以通过多种方式实现安排直接借记的功能;在极端情况下,移动应用程序可以向呼叫中心发送消息,让某人手动设置直接借记。但这种方式效率不高,扩展性也不强。

Direct debits is a capability you want to add to your product. You could implement the ability to arrange direct debits in a number of ways; in an extreme case, the mobile app could send a message to a call center where someone could set up the direct debit manually. But this wouldn’t be very efficient nor very scalable.

事实证明,实施直接借记取决于账户使用的货币。例如,实施欧元账户的直接借记与实施美元或英镑的直接借记截然不同,而且两者都是非同小可的工作——都无法在一次迭代中完成。“欧元账户的直接借记”和“美元账户的直接借记”将是不同的可交付功能。当团队准备好开发可交付功能时,他们会定义详细的验收标准并将其分解为用户故事。

It turns out that implementing direct debits depends on the currency used for the account. For example, the implementation of direct debits for Euro accounts is quite different to direct debits for USD or British Pounds, and both are nontrivial pieces of work—neither could be delivered in a single iteration. “Direct debit for Euro accounts” and “direct debit for USD accounts” would be different deliverable features. When the team is ready to work on a deliverable feature, they define detailed acceptance criteria and break it down into User Stories.

由于它本身很有用,并且他们可以从客户反馈中学习,因此您决定在下一个版本中包含“欧元直接借记”可交付功能。许多可交付功能组成一个版本。这些功能可能来自同一个 Epic,也可能来自一个版本的多个 Epic。

Because it is useful in its own right, and because they can learn from the customer feedback, you decide to include the “direct debit for Euro” deliverable feature in the next release. A number of deliverable features make up a release. These might come from the same Epic, or there might be features from several Epics in a release.

但从整个产品的角度来看,直接借记是一种连贯、内容丰富的功能。它是一个单一的产品功能。如果你正在编写用户手册或规范文档,你会有一章或一节专门介绍直接借记。最多可能有一个部分来描述不同货币账户所需的不同字段。从 BDD 的角度来看,将所有直接借记示例放在同一个地方是有意义的。如果你使用的是 Cucumber,那么你可以在一个功能中拥有所有的直接借记场景文件。

But from the point of view of the product as a whole, direct debits is a coherent, well-contained piece of functionality. It is a single product feature. If you were writing a user manual, or a specifications document, you would have a chapter or section for direct debits. At most you might have a section to describe different fields you need for different currency accounts. From a BDD perspective, it would make sense to have all your direct debit examples in the same place. And if you are using Cucumber, you would have all the direct debit scenarios in a single feature file.

5.2.6 并非所有事物都适合等级制度

5.2.6 Not everything fits into a hierarchy

在实际项目中,并非所有需求都符合我们一直在讨论的那种整齐的层次结构。虽然这适用于许多用户故事,但有时你会遇到支持多个功能的故事。例如,我们讨论的“注册时提供安全密码”故事可能与两个功能相关:

In real-world projects, not all requirements fit into the sort of neat hierarchical structures we’ve been talking about. Although this will work for many User Stories, you’ll sometimes come across a story that supports several features. For example, the “providing a secure password when registering” story we discussed might relate to two features:

  • 在线加入飞行常客计划

  • Join the Frequent Flyer program online

  • 确保客户数据安全

  • Keep client data safe

在这种情况下,您可能会说“在线加入飞行常客计划”功能是此用户故事的逻辑父级,但它显然也与跨职能功能“保证客户数据安全”相关。当涉及多个利益相关者时,这种情况经常发生。在这种情况下,业务利益相关者希望旅行者能够加入飞行常客计划,而安全利益相关者希望该功能能够安全地交付。可能还有其他利益相关者,例如合规、法律、运营等。

In that situation, you might say that the “join the Frequent Flyer program online” feature is a logical parent for this User Story, but it’s clearly also related to the cross-functional feature, “keep client data safe.” This often happens when multiple stakeholders are involved. In this case, the business stakeholder wants travelers to be able to join the Frequent Flyer program, and the security stakeholder wants the feature to be delivered safely. There may be other stakeholders as well, such as compliance, legal, operations, and so forth.

标签是处理这种情况的好方法。许多需求管理和报告工具都允许您使用标签来组织需求,此外还可以实现更传统的父子关系。这样,您可以呈现主要需求层次结构的相对结构化视图,同时还可以跟踪任何较松散的关系。我们将研究如何使用标签作为动态文档的一部分在第三部分书。

Tags are a good way to handle this sort of situation. Many requirements-management and reporting tools let you use tags to organize your requirements, in addition to enabling a more conventional parent–child relationship. This way, you can present a relatively structured view of the main requirements hierarchy, but also keep track of any looser relationships. We’ll look at using tags as part of the living documentation in part 3 of this book.

5.3真正的选择:在必要之前不要做出承诺

5.3 Real Options: Don’t make commitments before you have to

软件开发是一个探索之旅。在项目开始时,我们总会遇到一些未知的东西,但我们会一路探索。在软件开发中在开发过程中,了解你不知道什么,并在决策中主动管理你的无知是至关重要的。两个重要的 BDD 概念可以帮助我们做到这一点:真实选择和深思熟虑发现。

Software development is a journey of discovery. There are always things we don’t know at the start of a project, that we discover along the way. And in software development, it’s essential to know what you don’t know, and to proactively manage for your ignorance in your decisions. Two important BDD concepts can help us do just that: Real Options and Deliberate Discovery.

2000 年代中期,Chris Matts确定了许多敏捷实践背后的基本原则:将决策推迟到“最后责任时刻”,这一理念来自精益软件开发。他称此原则为 Real Options。理解此原则会改变您对许多敏捷实践的看法,并打开通往一些新实践的大门。

In the mid-2000s, Chris Matts identified a fundamental principle underlying many Agile practices: putting off decisions until the “last responsible moment,” an idea that comes from lean software development. He called this principle Real Options. Understanding this principle changes the way you think about many Agile practices and opens the door to a few new ones.

在金融领域,一种选择让您有机会(但不是义务)在未来某个时间以今天的价格购买产品。例如,假设您很可能需要在未来三个月内购买大量钢材,而钢材价格目前正在上涨。您现在不想购买钢材,因为您不完全确定自己是否需要它;您希望在未来两个月的某个时间确切知道。但如果您再等几个月,钢材价格可能已经上涨,这意味着您将会亏损。为了摆脱这个困境,您可以购买期权,在未来三个月的某个时间以今天的价格购买钢材。如果钢材价格上涨,您仍然可以以今天的价格购买。如果价格下跌,或者您不需要钢材,您可以选择不使用该期权。您需要为该期权付费,但它只花费钢材总价的一小部分。这是值得的,因为它允许您在确定需要钢材之前不必承诺购买它。

In finance, an option gives you the possibility, but not the obligation, to purchase a product sometime in the future at today’s price. For example, imagine that there’s a high probability that you’ll need to purchase a large quantity of steel in the next three months, and that the price of steel is currently on the rise. You don’t want to buy the steel now, because you aren’t completely sure that you’ll need it; you expect to know for sure sometime in the next two months. But if you wait another few months, the price of steel might have gone up, which means that you’ll lose money. To get out of this conundrum, you can buy an option to purchase the steel sometime within the next three months, at today’s price. If the price of steel goes up, you can still buy at today’s price. And if the price goes down, or if you don’t need the steel, you can choose not to use the option. You need to pay for this option, but it only costs a fraction of the total price of the steel. It’s worthwhile because it allows you to not commit yourself to buying the steel until you’re sure you need it.

这一原则也适用于日常生活。当你购买机票时,你实际上是在购买一种旅行选择权:机票不要求你一定要旅行。但你为这种选择权支付的价格各不相同。想象一下,你最喜欢的航空公司提供从悉尼飞往惠灵顿的 600 美元机票,但如果你决定不旅行,这些便宜的机票是不可退款的。你不确定自己是否能成行,所以你选择了一张更贵的 800 美元机票,取消费为 25 美元。

This principle also applies in day-to-day life. When you buy a plane ticket, you’re actually buying an option to travel: the ticket places you under no obligation to travel. But the price you pay for this option varies. Imagine your favorite airline is offering tickets for $600 to go from Sydney to Wellington, but these cheaper tickets are nonrefundable if you decide not to travel. You’re not sure that you’ll be able to make the trip, so you opt for a more expensive $800 ticket, which has a $25 cancellation fee.

让我们来看看这里的数学计算。取消航班的选项将花费您额外的 200 美元。如果您可能会出行,那么对于您不太可能使用的选项,这笔钱可能很多,因此您可能更喜欢便宜的机票。但如果您认为有 50% 的可能性您无法飞行,您可能会乐意支付额外的 200 美元。如果您取消,您只会损失 225 美元(额外的 200 美元加上 25 美元的取消费),而如果您在选择更便宜的航班后取消,您将损失 600 美元。

Let’s look at the math here. The option to cancel the flight costs you an extra $200. If you’re likely to travel, this might be a lot to pay for an option you’re unlikely to use, so you might prefer the cheaper ticket. But if you think that there’s a 50% chance you won’t be able to fly, you may be happy to pay the extra $200. If you cancel, you’ll only lose $225 (the extra $200, plus the $25 cancellation fee), whereas if you cancel after opting for the cheaper flight, you’ll lose $600.

实物期权是 Chris Matts 发明的将这些原则应用于软件开发的应用(见图 5.12)。1 Chris将实物期权的原理总结为三点:

Real Options is an application of these principles to software development invented by Chris Matts (see figure 5.12).1 Chris summarizes the principles of Real Options in three simple points:

  • 选择是有价值的。

  • Options have value.

  • 期权到期。

  • Options expire.

  • 除非你知道原因,否则不要过早承诺。

  • Never commit early unless you know why.

图 5.12 实物期权可让你通过保留选择权来降低风险。

Figure 5.12 Real Options lets you reduce risk by leaving your options open.

让我们更详细地了解一下这些原则。

Let’s look at each of these principles in a little more detail.

5.3.1 期权有价值

5.3.1 Options have value

选项之所以有价值,是因为它们允许你在掌握足够的知识来确定最佳解决方案之前推迟承诺采用特定解决方案。在金融行业,可以精确计算选项的价值。软件开发并非如此,但一般来说,你对特定问题的最佳解决方案了解得越少,保留选项的价值就越大。

Options have value because they allow you to put off committing to a particular solution before you have enough knowledge to determine what solution is best. In the finance industry, the value of an option can be calculated precisely. This isn’t the case in software development, but, in general, the less you know about the optimal solution for a particular problem, the more value there is in being able to keep your options open.

选项也是有代价的。软件开发的代价是整合这种灵活性所需的努力。这个代价可能包括预先讨论可能的选项、添加抽象层以便更轻松地切换不同的实现、使应用程序的某些部分可配置等等。

Options also have a price. The price in software development is the effort involved in incorporating this flexibility. This price might involve discussing the possible options upfront, adding layers of abstraction to allow a different implementation to be switched in more easily, making certain parts of the application configurable, and so forth.

例如,假设你正在为一家充满活力的年轻创业公司建立一个新网站。创始人并不清楚他们预期的用户数量;他们知道一开始用户数量会很少,但他们雄心勃勃,预计到今年年底用户数量将达到数百万。

For example, suppose that you’re building a new website for a dynamic young start-up. The founders have no clear idea of the volume of users they expect; they know it will start small, but they’re very ambitious and expect millions by the end of the year.

这里有三个选项。您可以构建应用程序而不特别考虑可扩展性,并在需要时使用 YAGNI(您不需要它)原则使其更具可扩展性。如果应用程序永远无法扩展,那么这很好。但如果需要扩展,重构工作将非常繁重。

You have three options here. You could build the application with no particular regard to scalability and make it more scalable if and when the need arises, using the YAGNI (you ain’t gonna need it) principle. This is fine if the application never scales. But if it does, the refactoring work will be extensive.

或者,你可以从一开始就投资于高度可扩展的架构。这可以避免返工,但如果数量仍然很少,那么这将是浪费精力。

Alternatively, you could invest in a highly scalable architecture from the get-go. This would avoid rework, but it would be wasted effort if volume remains low.

第三种可能性是购买以后扩展的选项。您不会立即实现完全可扩展的架构,但您可以提前花一点时间看看需要什么才能使初始实现在将来需要时轻松扩展。如果您不需要扩展,那么您只需要投入一点前期设计时间,如果需要扩展,您将能够以较低的成本完成扩展成本。

A third possibility is to buy an option to scale up later. You wouldn’t implement a fully scalable architecture immediately, but you could spend a little time upfront to see what would be needed to make the initial implementation easily scalable in the future if required. If you don’t need to scale, you’ve only invested a little upfront design time, and if you do, you’ll be able to do so at reduced costs.

5.3.2 期权到期

5.3.2 Options expire

不能永远保留一个选项。在软件开发中,当您在相关功能交付之前没有时间实现它时,选项就会过期(即您无法再使用它)。例如,在图 5.13 中,您可以在两种实现(解决方案 A 和解决方案 B)之间进行选择。此时,您不知道哪种解决方案最好,因此您添加了一层代码,以便以后可以切换到解决方案 A 或 B。

You can’t keep an option open forever. In software development, an option expires (i.e., you can no longer use it) when you no longer have time to implement it before the related feature is due to be delivered. For example, in figure 5.13, you have the choice between two implementations (solution A and solution B). At this point, you don’t know which solution is best, so you add a layer of code to make it possible to switch to either solution A or B at a later date.

图 5.13 实物期权到期。一旦过了期权的到期日,您就不能再行使该期权。

Figure 5.13 Real Options expire. Once you pass an option’s expiry date, you can no longer exercise this option.

如果您决定采用解决方案 A,则需要 10 天才能完成集成。而实施解决方案 B 则仅需 5 天。实际上,这意味着如果您决定实施解决方案 A,则必须在交货日期前至少 10 天实施,届时您对解决方案 A 的选择权将到期。如果您再拖延,您将无法行使此选项。您还有更多时间选择解决方案 B,因为此选项仅在交货日期前 5 天到期。

If you decide on solution A, it will take 10 days to integrate. Implementing solution B, on the other hand, would only take 5 days. In practical terms, this means that if you decide to implement solution A, you must do so at least 10 days before the delivery date, which is when your option on solution A expires. If you delay any further, you won’t be able to exercise this option. You have a bit more time to opt for solution B, as this option expires only 5 days before the delivery date.

与财务期权不同,您有时有权推迟到期日。例如,如果您能找到一种更快地集成解决方案 A 的方法,则可以保留该选项更长。

Unlike financial options, you sometimes have the power to push back expiry dates. For example, if you can find a way to integrate solution A more quickly, you can leave that option open longer.

5.3.3 除非你知道原因,否则不要过早提交

5.3.3 Never commit early unless you know why

实物期权的第三项原则就是推迟承诺某个解决方案(行使期权),直到你对选择该解决方案的原因有了足够的了解。实物期权让你可以推迟做出决定,但目的不是系统地拖延到最后可能(或负责任)的时刻。使用实物期权,你只需拖延到有足够的信息采取行动。当你有足够的信息时,你会尽快实施你选择的解决方案。例如,你可能会推迟你的决定,因为解决方案 A 正在由另一个团队开发,你想等着看这个库是否适合你的情况。在这种情况下,你可以等到解决方案 A 的期权到期,但不能再久了。如果库的开发时间比这更长,你将被迫将解决方案 A 从你的选项列表中排除。

The third principle of Real Options is simply to defer committing to a particular solution (exercise the option) until you know enough about why you’re choosing that solution. Real Options give you the possibility to put off making a decision, but the aim is not to systematically delay until the last possible (or responsible) moment. With Real Options, you only delay until you have enough information to act. When you have enough information, you implement your chosen solution as quickly as possible. For example, you may delay your decision because solution A is being developed by another team, and you want to wait to see if this library will work well in your situation. In this case, you can wait until the option for solution A expires, but no longer. If the library takes longer than this to develop, you’ll be forced to exclude solution A from your list of options.

但如果可以,您可以选择尽早采取行动。例如,如果您的团队可以构建解决方案 A 或解决方案 B,但您不知道哪个最合适,您可以选择同时构建两个解决方案的实验版本。如果您在选项到期之前获得足够的信息来采取行动,那么尽早采取行动是有意义的。为了帮助您了解足够的信息以做出明智的设计和实施决策,您可以使用一种称为刻意的发现。

But you may choose to act sooner if you can. For example, if your team can build either solution A or solution B, but you don’t know which is the most appropriate, you might choose to build experimental versions of both solutions concurrently. If you obtain enough information to act before the options expire, it makes sense to act sooner rather than later. To help you learn enough to make sensible design and implementation decisions, you can use an approach called Deliberate Discovery.

5.4 故意发现

5.4 Deliberate Discovery

商榷 发现是真实期权的另一面,这两个原则相辅相成。深思熟虑的发现最初由 Dan North 2提出,并由 Liz Keogh 和伦敦 BDD 社区的其他成员开发,它是一种软件开发方法,鼓励我们承认和接受自己的无知,以及我们将随着项目的进展而学习东西的事实,以便我们能够更好地准备应对变化和意外情况。

Deliberate Discovery is the flip side of Real Options, and the two principles go hand-in-hand. Originally proposed by Dan North2 and developed by Liz Keogh and other members of the London BDD community, Deliberate Discovery is an approach to software development that encourages us to acknowledge and embrace our own ignorance, and the fact that we will learn things as the project progresses, so that we might be better prepared to respond to changes and surprises when they occur.

在软件开发中,无知是一种制约因素。在完成构建特定解决方案后,您会对构建该解决方案的最佳方法有更多了解,但那时利用您的知识为时已晚。您可以使用实物期权原则推迟选择特定实现,或实施特定功能或故事,直到您了解足够多的信息以做出合理的决策。但如果您意识到自己不知道最佳解决方案是什么,您可以主动调查您的选择,以便尽早做出合理的决策。不确定性代表风险,在可能的情况下,您应该寻找并减少不确定性。这就是深思熟虑的发现介入的地方。

In software development, ignorance is the constraint. You know a lot more about the best way to build a particular solution after you’ve finished building it, but by then it’s too late to take advantage of your knowledge. You can use the principles of Real Options to put off choosing a particular implementation, or implementing a particular feature or story, until you know enough to make a reasonable decision. But if you’re aware that you don’t know what the best solution is, you can proactively investigate your options in order to make a reasonable decision sooner rather than later. Uncertainty represents risk, and where possible you should hunt out and reduce uncertainty. This is where Deliberate Discovery steps in.

刻意探索始于假设有些事情你不知道。这可能是一些你无法预料的坏事,它会在项目进行过程中的某个时刻突然出现并给你带来麻烦。或者这可能是一个创新的机会:“如果我们早点知道这项技术,我们就可以在一半的时间内开发出这个功能。”

Deliberate Discovery starts with the assumption that there are things you don’t know. This might be something bad that you couldn’t possibly have anticipated and that will pop up and cause you problems at some point during the project. Or it might be an opportunity to innovate: “If only we’d known about that technology earlier, we could have built this feature in half the time.”

真实选择可以帮助您保留选择,直到您有足够的信息采取行动;深思熟虑的发现可以帮助您获取这些信息。如果您积极尝试增加特定领域的知识,您既可以降低不确定性的风险,又可以更快地做出决策;请记住,只要您知道足够多的信息来致力于某个特定解决方案,您就可以选择是否行使您的选择权。

Real Options help you keep your options open until you have enough information to act; Deliberate Discovery helps you get this information. If you actively try to increase your knowledge in a specific area, you can both reduce the risk of uncertainty and make decisions faster; remember, as soon as you know enough to commit to a particular solution, you can choose to exercise your option or not.

但深思熟虑的发现也有更广泛的应用。例如,假设你决定实现一个特定的功能,并将其分解成多个故事。其中一些故事可能看起来很简单,而另一些故事可能不那么简单。

But Deliberate Discovery also has broader applications. For example, suppose you’ve decided to implement a particular feature and have broken it down into a number of stories. Some of these stories may seem straightforward, and others may not be so simple.

自然的倾向是先实现最简单的故事,这样做有充分的理由。但减少你的无知应该是你的首要任务。尽可能找出涉及最多不确定性的故事,并首先解决这些故事。然后回顾剩余的故事,记住你学到的东西,并考虑利益相关者给你的反馈。这种简单的方法可以大大帮助你增加对相关领域的知识和理解事情。

The natural tendency is to implement the simplest stories first, and there are good reasons why you might do this. But reducing your ignorance should be high on your priority list. Wherever possible, identify the stories that involve the most uncertainty, and tackle these ones first. Then review the remaining stories, keeping in mind what you’ve learned and considering the feedback the stakeholders give you. This simple approach can go a long way in helping you increase your knowledge and understanding in areas that matter.

5.5 使用 BDD 进行发布和冲刺规划

5.5 Release and sprint planning with BDD

真实的选项和深思熟虑的发现告诉我们,在实际工作之前进行过于详细的规划是一种浪费。更有效的做法是先在高层次上进行广泛的规划,然后在我们确定需要它们时再深入规划个别史诗和功能。

Real Options and Deliberate Discovery show us that planning in too much detail, too far ahead of the actual work, is wasteful. It is more effective to first plan broadly at a high level, and to drill down into planning individual Epics and features when we are sure that we will need them.

让我们回顾一下推测和说明阶段的各种活动如何帮助团队规划和交付新功能。图 5.14 概述了此过程。3

Let’s recap to see how the various activities in the Speculate and Illustrate phases can help teams plan and deliver new features. Figure 5.14 gives an overview of this process.3

图 5.14 BDD 和发布计划

Figure 5.14 BDD and release planning

战略规划活动期间,您发现并描述业务目标以及支持这些目标的功能,并使用影响图和海盗画布等技术来确定我们想要验证的假设以及可能证明或反驳这些假设的可交付成果。我们将这些可交付成果称为 Epic 或产品待办事项,它们将进入产品待办事项。在战略规划期间,团队还会优先考虑 Epic,以便规划即将发布的版本。

During strategic planning activities, you discover and describe the business goals and the capabilities that support them, and you use techniques like Impact Mapping and Pirate Canvases to identify hypotheses that we want to validate and deliverables that might prove or disprove these hypotheses. We call these deliverables Epics or Product Backlog items, and they go into the Product Backlog. During strategic planning, teams also prioritize Epics in order to plan upcoming releases.

在产品待办事项细化过程中,团队将计划在下一版本中发布的 Epic 分解为功能。根据项目规模,这可以在团队级别(对于较小的项目)或跨多个团队(对于较大的项目)进行。这些功能将进入各个团队的待办事项。

During Product Backlog refinement, teams break down the Epics scheduled for the next release into features. Depending on the size of the project, this can happen at either a team level (for smaller projects) or across multiple teams (for larger ones). These features go into individual team backlogs.

在说明阶段(我们将在下一章中讨论),团队从待办事项列表中选取一个功能,识别并澄清关键的功能和非功能验收标准(我们称之为场景),并将功能分解为用户故事。在制定阶段(我们将从第 6 章开始讨论),我们将了解如何将这些场景转化为可执行规范,作为每个冲刺或迭代期间完成的开发工作的基础。

During the Illustrate phase, which we will look at in the next chapter, a team takes a feature from their backlog, identifies and clarifies key functional and nonfunctional acceptance criteria (which we call scenarios), and breaks the feature down into User Stories. And during the formulate phase (which we will look at from chapter 6 and on) we will see how to turn these scenarios into executable specifications that act as the basis for the development work done during each sprint or iteration.

重要的是要记住,图 5.14 中的图表不是一个严格的流程,而是一个发现和验证业务假设的框架。反馈周期存在于各个级别;例如,如果在产品待办事项细化期间假设无效,那么团队可以自由地修正他们的目标和优先级立即地。

It’s important to remember that the diagram in figure 5.14 isn’t a rigid process but more a framework for discovering and validating business hypotheses. Feedback cycles exist at all levels; for example, if a hypothesis is invalidated during Product Backlog Refinement, then the teams are free to correct their goals and priorities immediately.

概括

Summary

  • 描述和组织特性并用示例说明它们,是 BDD 的重要方面。

  • Describing and organizing features, and illustrating them with examples, are important aspects of BDD.

  • 无论实施情况如何,能力都可以实现某些业务目标。

  • A capability enables some business goal, regardless of implementation.

  • 功能是可交付的软件功能的一部分,它为用户提供某种能力。

  • A feature is a piece of deliverable software functionality that provides users with a capability.

  • 您可以将大功能分解为较小的功能,以便于组织和交付。

  • You can break large features down into smaller features to make them easier to organize and deliver.

  • 敏捷项目使用用户故事来规划和交付功能,使用功能来规划和交付发布。

  • Agile projects use User Stories to plan and deliver features, and features to plan and deliver releases.

  • 实物期权原理建议您不要承诺某个特定的解决方案,除非您有足够的信息确信它是最合适的解决方案。

  • The principle of Real Options recommends that you shouldn’t commit to a particular solution until you have enough information to be confident that it’s the most appropriate one.

  • Deliberate Discovery 指出,任何软件项目中最大的风险之一就是您自己的无知,因此您应该积极主动地尽可能地识别和减少不确定性。

  • Deliberate Discovery points out that one of the biggest risks in any software project is your own ignorance, and that you should actively aim to identify and reduce uncertainty wherever you can.

BDD 的主要好处之一是鼓励和组织业务对话。但通过以自动化验收标准的形式自动化这些示例,也可以获得很多好处。在下一章中,您将了解可用于促进这些示例的实用技术对话。

One of the principal benefits of BDD is to encourage and structure business conversations. But there’s also a great deal to gain by automating these examples, in the form of automated acceptance criteria. In the next chapter, you will learn about practical techniques you can use to facilitate these conversations.


1  Chris Matts 和 Olav Maassen,“‘实物期权’是敏捷实践的基础”,InfoQ (2007),http://www.infoq.com/articles/real-options-enhance-agility

1  Chris Matts and Olav Maassen, “‘Real Options’ Underlie Agile Practices,” InfoQ (2007), http://www.infoq.com/articles/real-options-enhance-agility.

2  Dan North,“介绍刻意发现”,2010 年 8 月 30 日,http://dannorth.net/2010/08/30/introducing-deliberate-discovery

2  Dan North, “Introducing Deliberate Discovery,” August 30, 2010, http://dannorth.net/2010/08/30/introducing-deliberate-discovery.

3  在 XSCALE 中,这种布局称为 3D-Kanban,我们将在后面的章节中讨论。

3  In XSCALE, this layout is known as a 3D-Kanban, which we will address in a later chapter.

6 举例说明特点

6 Illustrating features with examples

本章封面

This chapter covers

  • 需求发现研讨会和三个朋友
  • Requirements discovery workshops and the Three Amigos
  • 使用表格来表示示例
  • Using tables to represent examples
  • 示例映射
  • Example Mapping
  • 特征映射
  • Feature Mapping
  • OOPSI 模型
  • The OOPSI model

在开始开发某个功能或故事之前,敏捷团队会聚在一起讨论需求。有些团队会在待办事项细化或 Sprint 规划会议期间进行这些对话,而其他团队则会专门召开需求发现研讨会。这些对话的目的是了解、定义并商定团队打算开发的功能(或用户故事)的验收标准。

Before work starts on a feature or story, Agile teams come together to talk through the requirements. Some teams have these conversations during Backlog Refinement or Sprint Planning sessions, while others have dedicated requirements discovery workshops. The aim of these conversations is to understand, define, and agree on the acceptance criteria of the features (or User Stories) that the team intends to work on.

实践 BDD 的团队会讨论业务规则的具体示例和反例,以制定这些验收标准。就我们在第 1 章中看到的 BDD 周期而言,这些对话在说明阶段很常见,说明阶段是需求发现过程的第二部分,始于我们在前几章中看到的推测活动。

Teams that practice BDD have conversations about concrete examples and counterexamples of business rules to come up with these acceptance criteria. In terms of the BDD cycle we saw in chapter 1, these conversations are common during the Illustrate phase, the second part of the requirements discovery process that starts with the Speculate activities we saw in the previous chapters.

虽然这在理论上很有道理,但许多团队发现进行这些对话并不像看起来那么容易。人们并不总是问正确的问题,他们会偏离主题,会议可能会拖延而似乎没有任何进展。因此,许多团队发现很难像他们应该的那样经常举行这些会议,或者发现他们的效率远没有他们希望的那样高。

While this makes perfect sense in theory, many teams find that having these conversations is not as easy as it might seem. People don’t always ask the right questions, they get side-tracked, and the sessions can drag on without seeming to go anywhere. As a result, many teams find it a challenge to run these sessions as often as they should or find that they aren’t anywhere near as productive as they would like them to be.

在本章中,我们将跟随我们的常旅客团队,他们使用对话和示例将高级功能分解为具有明确验收标准的较小用户故事。我们将看到我们的团队使用三种流行且有效的技术来促进这些对话:使用表格、示例映射和功能映射。所有这三种技术都是直观的,易于向非技术利益相关者解释,是让他们参与并谈论他们的需求的好方法。

In this chapter we will follow our Frequent Flyer team as they use conversations and examples to break high-level features down into smaller User Stories with well-defined acceptance criteria. We will see our team use three popular and effective techniques to facilitate these conversations: working with tables, Example Mapping, and Feature Mapping. All three of these techniques are intuitive and easy to explain to nontechnical stakeholders and are a great way to get them engaged and talking about their requirements.

令人惊讶的是,BDD 的许多好处都来自于与客户或业务利益相关者的对话,使用示例来挑战假设并建立对问题空间的共同理解。BDD 的主要好处之一是鼓励和组织这种对话。但是,通过以自动化验收标准的形式自动化这些示例也可以获得很多好处。但在我们研究这些技术之前,我们需要讨论这些对话是在何时以及与谁进行的。

A surprising number of BDD’s benefits come from simply having a conversation with customers or business stakeholders, using examples to challenge assumptions and build a common understanding of the problem space. One of the principal benefits of BDD is to encourage and structure this kind of conversation. But there’s also a great deal to gain by automating these examples, in the form of automated acceptance criteria. But before we look at any of these techniques, we need to talk about when and with whom these conversations take place.

6.1 三个朋友和其他需求发现研讨会

6.1 The Three Amigos and other requirements discovery workshops

对话需求发现可以有多种形式。例如,一些团队发现在项目早期举行大型需求发现研讨会很有价值,整个团队(包括业务利益相关者)都参与其中。这些研讨会是尽早进行沟通的好方法,可以让团队对他们正在构建的功能达成共识,并制作出一套高质量的示例。缺点是,它们可能很难组织,而且在人力方面成本高昂。

Conversations about requirements can take many forms. For example, some teams find value in large requirements discovery workshops early in the project, where the whole team, including business stakeholders, is involved. These workshops are a great way to get communication happening sooner rather than later to give the team a shared understanding of the features they’re building and to produce a set of high-quality examples. On the downside, they can be hard to organize and are expensive in terms of people’s hours.

其他团队喜欢在 Backlog 细化或 Sprint 规划会议期间讨论故事和验收标准。这可能是了解待完成工作范围的好方法,但有时没有足够的时间来详细讨论所有故事。

Other teams like to discuss stories and acceptance criteria during Backlog Refinement or Sprint Planning sessions. This can be a good way to get an overview of the scope of work to be done, but sometimes there is not enough time to cover all the stories in enough detail.

许多团队使用的一种流行方法被称为“三个朋友”。在这种方法中,少数具有不同观点的团队成员(通常是开发人员、测试人员和业务分析师或产品所有者)聚在一起讨论功能并在实际实施开始之前绘制示例。为了使这种方法有效,这三个人都需要相当熟悉问题空间,但每个角色的动态交互通常非常有成效。测试人员非常注重细节并注重验证,会提出模糊的边缘情况,并经常指出其他团队成员错过的场景。开发人员会指出技术考虑因素,业务分析师或产品所有者将能够判断不同场景的相关性和相对价值。此外,开发人员将逐渐获得比传统项目中通常更深入的业务需求理解,并且随着项目的进展,这种理解会变得越来越有用。

One popular approach used by many teams is known as the “Three Amigos.” In this approach, a small number of team members with different perspectives—often a developer, a tester, and a business analyst or product owner—get together to discuss a feature and draw the examples before any actual implementation starts. For this to work well, all three need to be reasonably familiar with the problem space, but the dynamic interaction of each role is often very productive. The tester, with great attention to detail and a focus on validation, will propose obscure edge cases and often point out scenarios that the other team members have missed. The developer will point out technical considerations, and the business analyst or product owner will be able to judge the relevance and relative value of the different scenarios. In addition, the developer will gradually obtain a much deeper understanding of the business requirements than would normally happen in a more traditional project, and this understanding becomes more and more useful as the project progresses.

有时,在这种方法中,三人甚至会坐在电脑前,一起编写自动化场景的初稿。这有助于降低以后信息丢失的风险,并且对于拥有一套完善的可执行规范的成熟团队或对 Gherkin 和业务领域都非常熟悉的从业者来说,这种方法非常有效。但是,在需求发现会话中直接编写给定...何时...然后场景可能会很耗时,并分散团队成员对大局的注意力。出于这个原因,许多团队更喜欢在需求发现会话(在说明阶段)期间使用更多广度优先的实践,例如功能映射和示例映射,并将给定...何时...然后留给制定阶段的一个较小的组(见图 6.1)。

Sometimes in this approach, the three will even sit around a computer and write an initial draft of the automated scenarios together. This helps reduce the risk of information loss later and can work well in mature teams with a well-established set of executable specifications or for practitioners who are very fluent in both Gherkin and the business domain. However, writing Given ... When ... Then scenarios directly in the requirements discovery session can be time-consuming and distract team members from the bigger picture. For this reason, many teams prefer to use more breadth-first practices such as Feature Mapping and Example Mapping during the requirements discovery sessions (in the Illustrate phase) and leave the Given ... When ... Then to a smaller group in the Formulate phase (see figure 6.1).

图 6.1 示例和特征映射通常发生在说明阶段。

Figure 6.1 Example and Feature Mapping typically happen during the Illustrate phase.

在一些团队中,业务分析师更喜欢自己完成大部分场景编写工作,在将问题交给开发团队实施之前,先咨询利益相关者是否有任何问题。经验丰富的从业者通常不推荐这种方法,因为它无法像以前的策略那样有效地建立共识。可执行规范(例如 Given ... When ... Then 场景)应被视为对话的输出,而不是输入。

In some teams, the business analyst prefers to do the bulk of the scenario writing, referring to stakeholders if they have any questions, before passing them to the development team to implement. Experienced practitioners generally don’t recommend this approach, as it fails to build a shared understanding as effectively as the previous strategies. Executable specifications, such as Given ... When ... Then scenarios, should be considered an output of the conversations, not an input.

6.2 举例说明特征

6.2 Illustrating features with examples

示例是 BDD 的核心。在与用户和利益相关者的对话中,BDD 从业者使用具体示例来加深对各种规模的功能和用户故事的理解,同时也消除和澄清不确定的领域(见图 6.2)。这些示例以企业可以理解的语言表达,以非常精确和明确的术语说明了软件应该如何运行。

Examples are at the heart of BDD. In conversations with users and stakeholders, BDD practitioners use concrete examples to develop their understanding of features and User Stories of all sizes, but also to flush out and clarify areas of uncertainty (see figure 6.2). These examples, expressed in language that businesses can understand, illustrate how the software should behave in very precise and unambiguous terms.

图 6.2 BDD 的发明者 Dan North 所言的 BDD 的本质

Figure 6.2 The essence of BDD, according to its inventor Dan North

根据 David Kolb 的说法实验学习理论认为,有效学习是一个四阶段的过程。1在Kolb的模型中,我们都是从一些现实世界的情况或事件的具体经验开始学习的(经验)当我们观察并思考一个经历(反思)时),我们分析并概括该示例,形成一个代表我们当前对问题空间的理解的心理模型(概念化最后,我们可以用其他现实世界的经验来测试这个心智模型,以验证或否定我们全部或部分理解(测试)。

According to David Kolb’s experimental learning theories, effective learning is a four-stage process.1 In Kolb’s model, we all start learning from concrete experiences of some real-world situations or events (experience). When we observe and think about an experience (reflection), we analyze and generalize that example, forming a mental model that represents our current understanding of the problem space (conceptualize). Finally, we can test this mental model against other real-world experiences to verify or invalidate all or part of our understanding (test).

BDD 采用非常类似的方法(见图 6.3),其中示例和与用户、利益相关者和领域专家的对话推动了学习过程。您讨论应用程序应如何运行的具体示例,并反思这些示例以建立对需求的共同理解。然后,您寻找其他示例来确认或扩展您的理解。

BDD uses a very similar approach (see figure 6.3), where examples and conversations with users, stakeholders, and domain experts drive the learning process. You discuss concrete examples of how an application should behave and reflect on these examples to build up a shared understanding of the requirements. Then you look for additional examples to confirm or extend your understanding.

图 6.3 David Kolb 的实验学习理论非常适用于 BDD。

Figure 6.3 David Kolb’s experimental learning theories apply well to BDD.

让我们看看这在实践中是如何运作的。故事卡和记在背面的初始验收标准(或者如果你使用软件来跟踪你的积压工作,则记在某个专用字段中)是开始对话的好地方,对话将发现这些示例。要了解这样的对话可能是什么样子,让我们重新回顾一下我们在第 5 章中讨论的“安全密码”用户故事。Bianca(业务分析师)、Terri(测试人员)和 David(开发人员)在 Frequent Flyer 项目中工作,他们正在讨论需求。故事是这样的:

Let’s see how this works in practice. The story card and the initial acceptance criteria jotted down on the back (or in some dedicated field if you are using software to track your backlog) make a great place to start a conversation that will discover these examples. To see what such a conversation might look like, let’s revisit the “secure password” User Story we discussed in chapter 5. Bianca (the business analyst), Terri (the tester), and David (the developer), who work on the Frequent Flyer project, are discussing the requirements. The story goes like this:

故事:注册飞行常客计划时提供安全密码
为了避免黑客入侵会员账户
作为系统管理员
我希望新会员在注册时提供安全密码
Story: Providing a secure password when registering for the Frequent Flyer program
In order to avoid hackers compromising member accounts
As the systems administrator
I want new members to provide a secure password when they register

Bianca 已经准备好了一套初步的验收标准:

Bianca has already prepared an initial set of acceptance criteria:

  • 密码至少应包含八个字符。

  • The password should be at least eight characters.

  • 密码应至少包含一位数字。

  • The password should contain at least one digit.

  • 密码至少应包含一个标点符号。

  • The password should contain at least one punctuation mark.

  • 如果我输入了不安全的密码,我应该收到一条错误消息,告诉我我做错了什么。

  • I should get an error message telling me what I did wrong if I enter an insecure password.

这些验收标准是一个好的开始,但仍然存在一些潜在的歧义。您可以全部使用小写字母,还是需要大小写混合?密码中数字的位置重要吗?错误消息应该有多详细?为了弄清楚这些问题,团队与系统管理员 Sid 进行了交谈。

These acceptance criteria are a good start, but there are still some potential ambiguities. Can you have all lowercase characters, or do you need a mixture of uppercase and lowercase? Does the position of the number in the password matter? How detailed should the error message be? To clear things up, the team talks to Sid, the systems administrator.

榜样的力量

The power of examples

在 BDD 中,我们使用示例来尝试澄清此类问题,因为我们很少会考虑所有事情。您可以使用一些关键示例作为正式验收标准的基础(我们将在下一章讨论如何以更结构化的方式表达验收标准)。您在这些对话中发现的示例并非都会出现在场景中 - 许多示例仅有助于指导对话并扩展您对问题空间的理解。

In BDD, we use examples to try to clarify questions like these, because we rarely think of everything. You can use a few key examples as the basis for your formal acceptance criteria (we’ll discuss how to express acceptance criteria in a more structured way in the next chapter). Not all of the examples that you’ll discover in these conversations will make it into the scenarios—many will simply be useful to guide the conversation and expand your understanding of the problem space.

如果您使用一些简单的策略,这种对话会更有成效。请记住,此练习的目的是构建需求的心理模型,并用一些关键示例来说明该心理模型。将故事的问题空间视为一组拼图碎片。当您要求举例时,您实际上是在要求澄清您对需求的理解。这就像拿起一块拼图,并将其放在您认为应该放的地方。如果它合适,您就确认了您的理解并扩展了您的心理模型。如果不合适,那么您就消除了一个错误的假设,并可以在更坚实的基础上继续前进。

This sort of conversation is more productive if you use some simple strategies. Remember, the aim of this exercise is to build a mental model of the requirements and to illustrate this mental model with a number of key examples. Think of the problem space for the story as a set of jigsaw puzzle pieces. When you ask for an example, you’re really asking for clarification of your understanding of the requirements. This is like picking up a piece of the jigsaw and placing it where you think it should go. If it fits, you’ve confirmed your understanding and expanded your mental model. If it doesn’t, then you’ve flushed out an incorrect assumption and can move forward on a more solid basis.

Sid 是系统安全专家,非常了解如何设置安全的密码。Sid 非常关心这个问题,因为根据他的经验,大多数用户自然会使用非常容易破解的密码。与 Sid 的对话大致如下:

Sid is an expert in system security and knows a great deal about what makes a secure password. Sid is very concerned about this problem, as in his experience most users naturally use passwords that are very easy to hack. The conversation with Sid goes along the following lines:

Bianca:我想确认一下我是否理解了“安全密码”故事中你需要什么。我们定义的第一个验收标准是密码长度。如果密码少于八个字符,就应该被拒绝?

Bianca: I’d like to make sure I’ve understood what you need for the “secure password” story. The first acceptance criteria we defined is about password length. A password should be rejected if it has less than eight characters?

席德:是的,没错。密码至少需要八个字符,这样黑客算法才更难猜到。

Sid:      Yes, that’s right. Passwords need to be at least eight characters to make it harder for hacking algorithms to guess them.

比安卡:所以“秘密”会被拒绝,因为它只有六个字?

Bianca: So “secret” would be rejected because it has only six characters?

席德: 正确。

Sid:      Correct.

比安卡:那“密码”呢?可以接受吗?

Bianca: What about “password”? Would that be acceptable?

席德:不,我们还说过,我们至少需要一位数字。

Sid:      No, we also said that we need at least one digit.

比安卡:我们确实这么做了。那么“密码1”可以吗?

Bianca: So we did. So “password1” would be OK?

席德:不,实际上这仍然很容易被破解。这是字典里的一句话:末尾的数字不会长时间减慢黑客算法的速度。随机字母和标点符号会让破解变得有点困难。

Sid:      No, actually that’s still really easy to hack. It’s a word from a dictionary: the digit at the end wouldn’t slow down a hacking algorithm for very long. Random letters and punctuation marks make it a bit harder.

比安卡:好的,那么“密码 1!”可以吗?它有一个数字和一个感叹号,并且有八个以上的字符。

Bianca: OK, so would “password1!” be OK? It has a number and an exclamation mark, and it has more than eight characters.

席德:不,就像我说的,使用字典里的单词(比如“密码”)是非常糟糕的。即使使用数字和标点符号,黑客算法也能立即解决这个问题。

Sid:      No, like I said, using words from a dictionary like “password” is really bad. Even with numbers and punctuation, a hacking algorithm would solve that pretty much instantaneously.

注意这里刚刚发生的事情。Bianca 正在刻意测试她的假设,即初始验收标准代表了构成安全密码的所有约束。在每一步中,她都使用不同的示例来验证她对各种规则的理解。现在她发现了另一个她以前不知道的要求:应避免使用字典中的单词。她决定进一步推进这一点:

Notice what has just happened here. Bianca is deliberately testing her assumption that the initial acceptance criteria represent all the constraints that make a secure password. At each step, she used a different example to verify her understanding of the various rules. Now she has found another requirement that she wasn’t aware of before: dictionary words should be avoided. She decides to push this further:

比安卡:“海鸥刺猬”怎么样?

Bianca: How about “SeagullHedgehog”?

席德:那样就更好了。

Sid:      That would be better.

比安卡:但是里面没有数字或标点符号。

Bianca: But there are no numbers or punctuation marks in it.

席德:当然,这样会更好。但这仍然是一个随机的单词序列,很难破解。

Sid:      Sure, that would make it better. But it’s still a random sequence of words, which would be pretty hard to crack.

比安卡:“SeagullHedgehogCatapult” 怎么样?

Bianca: How about “SeagullHedgehogCatapult”?

席德:几乎无法破解。

Sid:      Pretty much uncrackable.

为了跟踪这些案例,测试人员 Terri 一直在记录一个简单的示例表,用于她的测试。以下是她目前所记录的内容:

To keep track of these cases, Terri the tester has been noting a simple table of examples to use for her tests. Here’s what she has so far:

密码

Password

安全的

Secure

秘密

secret

No

密码

password

No

密码1

password1

No

海鸥刺猬

SeagullHedgehog

是的

Yes

海鸥刺猬弹射器

SeagullHedgehogCatapult

是的

Yes

比安卡决定验证她的另一个假设:

Bianca decides to check another of her assumptions:

比安卡:好的,那么“aBcdEfg1”怎么样?

Bianca: OK, how about “aBcdEfg1”?

席德:这个密码对于机器来说其实很容易破解——它只是按字母顺序排列的字母序列和一个数字。序列很容易破解,而且在末尾添加一个数字不会增加太多复杂性。

Sid:      That one would actually be pretty easy for a machine to crack—it’s just a sequence of alphabetically ordered letters and a number. Sequences are easy to crack, and just adding a single number at the end doesn’t add much complexity.

比安卡:“qwertY12” 怎么样?

Bianca: What about “qwertY12”?

席德:那只是键盘上的按键序列。大多数黑客算法都知道这个技巧,所以很容易猜到。

Sid:      That’s just a sequence of keys on the keyboard. Most hacking algorithms know about that trick, so it would be very easy to guess.

比安卡:哦。好的,“dJeZDip1”怎么样?

Bianca: Oh. OK, how about “dJeZDip1”?

席德:那就有点短了,但是没关系。

Sid:      That would be a bit short, but OK.

现在团队有了新的要求:键盘上的字母顺序和按键的空间顺序都是不允许的。但 David 注意到了一些有趣的事情:

Now the team has a new requirement: alphabetical sequences of letters and spatial sequences of keys on the keyboard are both a no-no. But David has noticed something interesting:

大卫:席德,你似乎并不只是在评价我给你的密码是否安全,而是根据密码被破解的难易程度来评级——这是故意的吗?

David:  Sid, rather than just saying if a password I give you is secure or not, you seem to be grading them by how hard they are to hack—is that intentional?

席德:嗯,我可没这么想过,但确实如此:安全密码的意义在于不被黑客破解,而大多数人使用的密码都很容易被破解。2有很多研究和算法可以测量密码强度。3许多网站都会对您输入的密码强度提供反馈。从这个角度来看,我们需要密码至少具有中等强度。

Sid:      Well, I wasn’t thinking of it like that, but yes, of course: the whole point of a secure password is so that it doesn’t get hacked, and the passwords most people use are pretty easy to hack.2 There are a lot of studies and a lot of algorithms out there that measure password strength.3 And many sites provide feedback on the strength of the passwords you enter. In those terms, we need passwords to be of at least medium strength.

Sid 打开了一个类似图 6.4 的屏幕。

Sid brings up a screen similar to the one in figure 6.4.

图 6.4 密码安全计通过测量密码被破解的难易程度来帮助用户提供更安全的密码。

Figure 6.4 A password security meter helps users provide more secure passwords by measuring how easy a password would be to crack.

Bianca:Sid,我认为我们过于关注密码验证的详细规则。通过这些示例,似乎可以发现这些规则并不像我们最初想象的那样明确。我们所描述的规则侧重于我们试图解决的问题的一个特定解决方案:这个故事的真正价值在于确保用户拥有强密码,而不是强制执行一组特定的规则。如果我们从密码强度而不是具体规则的角度进行推理,也许我们可以重新表述验收标准,如下所示:

Bianca: Sid, I think we’ve been focusing on the detailed rules for password validation too much. Working through these examples seems to indicate that the rules are less clear-cut than we initially thought. And the rules we’re describing focus on one particular solution to the problem we’re trying to solve: the real value in this story comes from ensuring that users have a strong password, not enforcing a particular set of rules. If we reason in terms of password strength rather than specific rules, maybe we could rephrase the acceptance criteria like this:

  • 密码至少应具有中等强度才能被接受。
  • The password should be at least of medium strength to be accepted.
  • 我应该被告知我所提议的密码的强度。
  • I should be informed of the strength of my proposed password.
  • 如果密码太弱,我应该被告知原因。
  • If the password is too weak, I should be informed why.

席德:是的,听起来不错。但是我们如何知道什么才算是中等强度的密码(见图 6.5)?

Sid:      Yes, that sounds fine. But how do we know what qualifies as a medium-strength password (see figure 6.5.)?

图 6.5 密码强度并不像看起来那么简单(xkcd.com提供)。

Figure 6.5 Password strength is not as simple as it seems (courtesy of xkcd.com).

David:在我看来,我们这里有一些选择。我们可以编写自己的密码强度算法,也可以使用现有的算法。每种方法都有优缺点,但使用现有的库可能会更快实现。

David:  It looks to me like we have a few options here. We can either write our own password-strength algorithm or use an existing one. There are pros and cons to each approach, but using an existing library would probably be faster to implement.

Bianca:让我们保持开放的态度;我们目前还不清楚哪种方案最适合我们,因此我们只能尽力去了解更多。我们会看看能否找到一个好的现有库并尝试一下,但我们应该以一种方式来集成它,这样如果我们对找到的库不满意,以后就可以轻松切换到另一个库或我们自己的自定义解决方案。

Bianca: Let’s keep our options open; we don’t know enough about what will suit us best to commit to a particular solution just yet, so let’s do what we can to learn more. We’ll see if we can find a good existing library and experiment with it, but we should integrate it in a way that we can easily switch to another library or our own custom solution later on if we aren’t happy with the one we find.

David:Sid,我应该能够在周四之前使用几个可能的库构建一个版本,您可以试用一下。根据您的反馈,我们可以微调我们选择的解决方案或尝试另一个。

David:  Sid, I should be able to build a version of this using a couple of possible libraries by Thursday that you can play around with. Based on your feedback, we can fine-tune the solution we pick or try out another one.

Terri:我们可以将这张示例密码表作为验收标准的起点。随着我们进一步了解我们可以做什么,我们可能会对其进行改进或添加新示例。

Terri:  We can use this table of sample passwords as a starting point for the acceptance criteria. We may refine it or add new examples later as we learn more about what we can do.

密码

Password

力量

Strength

可接受

Acceptable

秘密

secret

虚弱的

Weak

No

密码

password

虚弱的

Weak

No

密码1

password1

虚弱的

Weak

No

乙丙橡胶

aBcdEfg1

虚弱的

Weak

No

qwertY12

qwertY12

虚弱的

Weak

No

dJeZDip1

dJeZDip1

中等的

Medium

是的

Yes

海鸥刺猬

SeagullHedgehog

强的

Strong

是的

Yes

海鸥刺猬弹射器

SeagullHedgehogCatapult

非常强劲

Very strong

是的

Yes

现在,团队已经从看似清晰而简单的需求集,发现真正的需求并不那么明显。最初似乎是用户请求的业务规则,结果只是确保成员拥有安全密码这一潜在业务问题的一种可能解决方案。团队确定了几种可能的方法,但推迟选择具体选项,直到他们更清楚他们将使用哪种解决方案。他们确定了一种团队可以用来从业务部门获得有用反馈的策略;这将帮助他们选择最合适的解决方案。

The team has now gone from having what appeared to be a clear and simple set of requirements to discovering that the real requirements are not quite so obvious. What initially appeared to be business rules requested by the user turned out to be just one possible solution to the underlying business problem of ensuring that members have secure passwords. The team identified several possible approaches but deferred choosing a specific option until they knew more about what solution they would use. And they identified a strategy that the team could use to get useful feedback from the business; this will help them select the most appropriate solution.

6.3 使用表格描述更复杂的需求

6.3 Using tables to describe more complex requirements

表格像我们在上一节中看到的那样,这可能是讨论更复杂需求的好方法。涉及处理或转换数据的业务规则在许多行业中很常见,并且这种数据通常很容易以表格形式表示和讨论。

Tables like the ones we saw in the previous section can be a good way to discuss more complex requirements. Business rules that involve processing or transforming data are common in many industries, and this kind of data is often easy to represent and discuss in tabular form.

例如,假设 Flying High Airlines 推出了一项特别优惠:当新的飞行常客会员加入该计划时,他们在过去 90 天内完成的航班将计入他们的飞行常客积分:

For example, suppose Flying High Airlines has launched a special offer: when new Frequent Flyer members join the program, flights that they have completed over the past 90 days count towards their Frequent Flyer points:

故事:加载以前的常旅客航班
作为新常旅客
我希望将我之前的航班与我的帐户关联
这样我就能更快地获得更多的飞行常客福利
Story: Loading previous Frequent Flyer flights
As a new Frequent Flyer
I want my previous flights to be associated with my account
So that I can get more Frequent Flyer benefits sooner

飞行高航可以找到每个人过去的航班的所有飞行记录,因此当会员注册时,此新功能需要识别符合条件的航班并相应地记入他们的飞行常客积分。

Flying High Airlines can find all the flight records for past flights for each person, so when a member signs up, this new feature needs to identify the eligible flights and credit their Frequent Flyer points accordingly.

为了进一步了解这一要求的细节,比安卡、特丽和大卫去和飞行常客计划经理弗雷德进行交谈。

To understand the finer points of this requirement, Bianca, Terri, and David go to talk with Fred, the Frequent Flyer program manager.

比安卡:所以我们的应用程序需要识别某个人最近的所有航班,并将其记入他们的新飞行常客帐户。我们应该追溯到多久以前?

Bianca: So our application needs to identify all the recent flights for a given person and credit them to their new Frequent Flyer account. How far back should we go?

弗雷德:我们需要包括报名日期前 90 天内的所有航班。

Fred:   We need to include any flights in the 90 days preceding the sign-up date.

为了跟踪讨论,比安卡绘制了一个简单的表格,其中显示了符合特别优惠条件的近期航班,以及太旧的航班的反例:

To keep track of the discussion, Bianca sketches a simple table that shows a recent flight that is eligible to be included in the special offer and a counterexample of a flight that is too old:

航班日期

Flight Date

有资格的

Eligible

60天前

60 days ago

是的

Yes

100天前

100 days ago

No

团队还同意 90 天期限从午夜开始。为了确保这条信息不会丢失,他们将其作为注释添加到表格下方。但 Terri 不确定他们是否已经涵盖了所有角度。

The team also agrees that the 90-day period starts at midnight. To make sure this piece of knowledge is not lost, they add it as a note underneath the table. But Terri isn’t sure they have covered all the angles just yet.

Terri:过去 90 天内的所有航班都符合条件吗?没有例外吗?

Terri:  Are all flights in the past 90 days eligible? Are there never any exceptions?

弗雷德:当然,有很多例外,但这些例外非常明显。例如,只有 Flying High 航班才有资格,合作伙伴或代码共享航班则不行。

Fred:   Sure, there are plenty of exceptions, but they are pretty obvious. For example, only Flying High flights are eligible, not flights on partner or codeshare flights.

大卫:那么我们如何区分 Flying High 航班与其他航班呢?

David:  And how do we distinguish between Flying High flights and the other ones?

弗雷德:Flying High 航班的航班号以“FH”开头。

Fred:   Flying High flights have a flight number that starts with “FH.”

这对团队来说是个新消息。通常,对用户或业务人员来说显而易见的业务规则对开发团队成员来说却不那么明显。Bianca 调整了她的表格,以包含一些符合条件和不符合条件的航班号示例:

This is news to the team. Oftentimes, business rules that seem obvious to users or business folk are less obvious to development team members. Bianca adjusts her table to include some examples of eligible and ineligible flight numbers:

航班

Flight Number

航班日期

Flight Date

有资格的

Eligible

原因

Reason

FH-99

FH-99

60天前

60 days ago

是的

Yes

 

 

FH-87

FH-87

100天前

100 days ago

No

太旧

Too old

OH-101

OH-101

60天前

60 days ago

No

并非高空飞行

Not a Flying High flight

这些例子简明扼要地说明了他们学到的新规则。但 Terri 还没有完成。

These examples succinctly illustrate the new rules they have learned. But Terri isn’t done yet.

Terri:好的,我们来关注 Flying High 航班。是否有我们可能需要处理的 Flying High 航班,即使它们处于正确的时间段,也不符合条件?

Terri:  OK, let’s focus on Flying High flights. Are there any Flying High flights that we might need to handle, that are not eligible even if they are in the correct time period?

弗雷德:嗯,如果航班取消了,就不算数。

Fred:   Well, if the flight was cancelled, it wouldn’t count.

比安卡:那些已经预订但还没有出发的航班怎么办?

Bianca: What about flights that are booked but haven’t happened yet?

弗雷德:不,你必须完成一次飞行才算数。

Fred:   No, you need to have completed a flight for it to count.

有了这些新规则,Bianca 可以在表格中添加另外两个示例。但如果她想让它们有意义,她还需要添加一个新列来指示航班状态:

With these new rules, Bianca can add two more examples to her table. But if she wants them to make sense, she also needs to add a new column to indicate the status of the flight:

航班

Flight Number

航班日期

Flight Date

地位

Status

有资格的

Eligible

原因

Reason

FH-99

FH-99

60天前

60 days ago

完全的

COMPLETED

是的

Yes

 

 

FH-87

FH-87

100天前

100 days ago

完全的

COMPLETED

No

太旧

Too old

OH-101

OH-101

60天前

60 days ago

完全的

COMPLETED

No

并非高空飞行

Not a Flying High flight

FH-99

FH-99

60天前

60 days ago

取消

CANCELLED

No

必须完成

Must be completed

FH-99

FH-99

5天后

In 5 days time

确认的

CONFIRMED

No

必定发生

Must have taken place

这些示例再次说明了团队发现的新规则。在下一章中,我们将了解如何将这样的表格转换为可执行格式,不仅可以说明需求,还可以证明我们的应用程序运行正确。

Once again, these examples illustrate the new rules the team has discovered. In the next chapter, we will see how we can turn tables like this into an executable format that not only serve to illustrate a requirement but can also demonstrate that our application behaves correctly.

表格简单、直观且易于使用。对于可以用明确定义的输入和输出来描述的问题,它们可以发挥奇效。围绕一组表格示例进行协作可以像在白板上书写或打开电子表格一样简单。但有时仅仅列出示例是不够的,而以数据为中心的表格方法有点过于死板。对于更复杂或不确定性更大的需求,我们需要更多的结构来指导我们的对话。在以下部分中,我们将介绍另外两种可以帮助我们促进这些对话的技术:示例映射和特征制图。

Tables are simple, intuitive, and easy to use. They work wonders for problems that can be described in terms of clearly defined inputs and outputs. And collaborating around a set of tabular examples can be as simple as writing on a whiteboard or opening a spreadsheet. But sometimes simply listing examples is not enough, and a tabular, data-centric approach is a little too rigid. For more complex requirements, or ones with more uncertainty, we need a little more structure to guide our conversations. In the following sections, we will look at two other techniques that can help us facilitate these conversations: Example Mapping and Feature Mapping.

6.4 示例映射

6.4 Example Mapping

例子映射4是一种简单、低技术含量的方法,团队可以通过它识别关键示例和反例,这些示例和反例最终将成为可执行规范中的自动化场景。这是一种快速、广度优先的方法,解释起来很快,易于学习,并且在引导对话朝正确方向发展方面非常有效。

Example Mapping4 is a simple, low-tech way for teams to identify key examples and counterexamples that will eventually become automated scenarios in the executable specifications. It is a fast, breadth-first approach that is quick to explain, easy to learn, and surprisingly effective at channeling conversations in the right direction.

在示例地图会议期间,团队确定与功能相关的关键业务规则或约束,并列出这些规则的示例和反例。示例地图使用排列在大型桌子上的彩色索引卡或张贴在墙壁或白板上的便签。分布式团队可以使用远程白板工具,例如 Miro ( https://miro.com ) 或 MURAL ( https://mural.co ),甚至通过 Skype 使用 Excel 或 PowerPoint。

During an Example Mapping session, teams identify the key business rules or constraints associated with a feature and list examples and counterexamples for these rules. Example Maps use colored index cards that you arrange on a large table or sticky notes that you post on a wall or whiteboard. Distributed teams can use remote whiteboarding tools such as Miro (https://miro.com) or MURAL (https://mural.co), or even Excel or PowerPoint over Skype.

这些卡片代表并记录了表 6.1 中所示的四个关键概念。

The cards represent and record four key concepts that are illustrated in table 6.1.

表 6.1 示例映射中使用的不同类型的卡片

Table 6.1 The different types of cards used during Example Mapping

卡片

Card

描述

Description

颜色

Color

例子

Example

用户故事

User Story

正在讨论的功能或用户故事

The feature or User Story under discussion

黄色的

Yellow

业务规则

Business rule

与用户故事相关的约束或已知验收标准

Constraints or known acceptance criteria related to the User Story

蓝色的

Blue

例子

Example

业务规则的具体示例和反例

Concrete examples and counterexamples of the business rule

绿色的

Green

问题

Question

会议期间无法回答或确认的不确定性或假设

Uncertainty or assumptions that cannot be answered or confirmed during the session

粉色的

Pink

6.4.1 示例映射从用户故事开始

6.4.1 Example Mapping starts with a User Story

一个示例映射会话以产品所有者(或业务分析师)介绍正在讨论的用户故事开始。请记住,用户故事不是需求规范,而是需要解决的问题的描述。用户故事写在板顶部的黄色索引卡上。例如,图 6.6 是我们在前几章中介绍的常旅客应用程序的用户故事。

An Example Mapping session starts with the product owner (or business analyst) presenting the User Story under discussion. Remember, the User Story is not a requirements specification, but more a description of a problem that needs solving. The User Story goes on a yellow index card at the top of the board. For example, figure 6.6 is a User Story from the Frequent Flyer application that we introduced in the previous chapters.

图 6.6 赚取飞行常客积分的用户故事

Figure 6.6 The earning Frequent Flyer points User Story

团队讨论用户故事,提出问题,并尝试确定与故事相关的关键业务规则或约束。业务分析师或产品所有者可能已经在会议之前以验收标准的形式记录了其中一些内容。对于这个故事,产品所有者已经记录了以下验收标准:

The team discusses the User Story, asks questions, and tries to identify the key business rules or constraints related to the story. The business analyst or product owner may have already noted some of these in the form of acceptance criteria before the session. For this story, the product owner has already noted the following acceptance criteria:

  • 欧洲境内航班可赚取 100 积分。

  • Flights within Europe earn 100 points.

  • 欧洲以外的航班每飞行 10 公里可赚取 1 点积分。

  • Flights outside Europe earn 1 point per 10 km flown.

  • 商务航班可额外赚取 50%。

  • Business flights earn an extra 50%.

团队首先将这些验收标准作为业务规则写在用户故事下面的蓝色卡片上(见图 6.7)。

The team starts by writing these acceptance criteria as business rules on blue cards underneath the User Story (see figure 6.7).

图 6.7 规则用蓝色卡片表示。

Figure 6.7 Rules are represented by blue cards.

6.4.2 寻找规则和例子

6.4.2 Finding rules and examples

下一个,团队讨论规则并要求为每个规则举一个例子。通常使用以“The one where ...”开头的短语来描述示例。这种表示法最初由 Daniel Terhorst-North 描述,被称为“老友记情节表示法”,来自 90 年代同名电视剧。根据 Terthorst-North 的说法,使用的第一个例子是“Joey 的头被卡在火鸡里”。例如,我们可以用“Tara 乘坐经济舱从巴黎飞往柏林”和“Tara 乘坐经济舱从伦敦飞往纽约”等例子来说明前两个规则。

Next, the team discuss the rules and asks for an example of each. Examples are often described using a short phrase that starts with the words “The one where ...” This notation, originally described by Daniel Terhorst-North, is known as the “Friends episode notation,” from the 90s TV series of the same name. According to Terthorst-North, the very first example used was “the one where Joey gets his head stuck in a turkey.” For instance, we could illustrate the first two rules with examples such as “the one where Tara flies economy from Paris to Berlin” and “the one where Tara flies economy from London to New York.”

您可以根据需要在示例卡中添加尽可能多的细节,但通常最好保持简洁明了。例如,如果结果不明显,您可以在卡片上使用箭头标注(见图 6.8)。您还可以使用项目符号来指示涉及多个输入或步骤的示例。

You can add as much detail as you need to the example cards, but it is generally better to keep them lightweight and succinct. For example, if the outcome isn’t obvious, you can note it on the card using an arrow (see figure 6.8). You can also use bullet points to indicate examples where several inputs or steps are involved.

图 6.8 示例以绿色卡片表示。

Figure 6.8 Examples are represented by green cards.

团队使用这些示例来提问并寻找反例。他们以“如果……会怎样?”“总是这样吗?”或“有没有不适用这条规则的例子?”开头提出问题。例如,关于商务航班的第三条规则需要澄清:旅行者乘坐商务航班总是能获​​得 150 积分吗?为了澄清这一点,我们可以再举一个例子,贝蒂乘坐商务舱从巴黎飞往香港。

The team uses these examples to ask questions and look for counterexamples. They ask questions starting with “What if ... ?” “Is this always the case?” or “Are there any examples where this rule does not apply?” For example, the third rule about business flights could use some clarification: is it always the case that a traveler earns 150 points for a business flight? To clarify this point, we could add another example where Betty flies business class from Paris to Hong Kong.

6.4.3 发现新规则

6.4.3 Discovering new rules

这些问题还可能导致发现新规则。例如,这些积分规则适用于所有航班吗?还是适用于所有客户?这些问题让产品所有者想起另一条规则:银卡常旅客会员可多获得 25% 的积分。有了这些信息,团队可以添加一条新规则和相应的示例(图 6.9)。

These questions can also lead to new rules being discovered. For example, do these point rules apply for all flights? Or for all customers? These questions lead the product owner to remember another rule: Silver Frequent Flyer members earn 25% more points. With this information, the team can add a new rule and a corresponding example (figure 6.9).

图 6.9 发现新规则

Figure 6.9 Discovering new rules

有时这些新规则可能超出了原始故事的范围。这可以帮助澄清故事的范围,并将故事分成可管理的部分。例如,团队可能决定将常旅客身份留作单独的故事,并只关注常客,以开始和。

Sometimes these new rules might be out of scope of the original story. This can help clarify the scope of the story and slice the story into manageable chunks. For example, the team might decide to leave Frequent Flyer status for a separate story and focus only on regular customers to start with.

6.4.4 浮现不确定性

6.4.4 Surfacing uncertainty

问题无法立即回答的问题被标记为粉色卡片。例如,在这种情况下,一个问题可能是关于“欧洲境内航班”的范围——这是否仅包括欧盟 (EU) 内的国家,还是也包括挪威等位于欧洲经济区 (EEA) 内的国家,或者还有其他定义?问题卡可以添加到地图上的任何位置;有时将它们添加到触发它们的示例或规则附近是有意义的,而其他时候它们只是简单地分组到地图的一侧(图 6.10)。

Questions that can’t be answered immediately are noted as pink cards. For example, in this case, one question might be about the scope of “flights within Europe”—does this include only countries within the European Union (EU) or also cover countries such as Norway, which are within the European Economic Area (EEA), or is there some other definition? Question cards can be added anywhere on the map; sometimes it makes sense to add them near the example or rule that triggered them, whereas other times they are simply grouped to one side of the map (figure 6.10).

图 6.10 问题以粉色卡片表示。

Figure 6.10 Questions are represented by pink cards.

其他问题可能需要产品所有者咨询其他利益相关者,或者团队可能需要进行一些研究或实验。例如,常旅客会员可以使用他们赚取的积分购买机票;他们是否也应该在用积分购买的机票上赚取积分?在这种情况下,产品所有者需要咨询营销团队。

Other questions might need the product owner to consult with other stakeholders, or the team might need to do some research or experimentation. For example, Frequent Flyer members can purchase flights with the points they earn; should they also earn points on flights that they purchase with their points? In this case, the product owner will need to check with the marketing team.

问题卡有助于保持对话流畅。如果您遇到不懂的问题,不要陷入讨论的泥潭;只需记下问题并继续前进!完整的示例地图如下所示图 6.11。

Question cards help keep the conversation flowing. If you come across something you don’t know, don’t get bogged down discussing it; simply note down the question and move on! The full Example Map looks like the one shown in figure 6.11.

图 6.11 完成的示例地图

Figure 6.11 A completed Example Map

6.4.5 促进示例映射会话

6.4.5 Facilitating an Example Mapping session

让某人主持示例映射会议通常很有用,尤其是当您不熟悉该技术时。很容易陷入对话中而忘记记录规则和示例。

It is often useful to have someone to facilitate Example Mapping sessions, especially when you are new to the technique. It can be easy to get caught up in the conversation and forget to record the rules and examples.

示例 地图绘制会议应该非常短;25-30 分钟通常足以讲完一个故事。一个有用的技巧是给会议设定时间限制,并让团队成员在时间限制结束时投票决定他们是否已经对故事有了足够的了解,可以开始处理它,或者是否需要另一个会议。

Example Mapping sessions should be quite short; 25–30 minutes is usually enough to get through a story. One useful technique is to timebox the session and to have team members vote at the end of the timebox on whether they have gained enough understanding of the story to start working on it or if another session is required.

示例映射是一种出色的技术,可用于在相对肤浅的层面上发现和讨论关键业务规则、示例和反例。它在很大程度上是一种广度优先的方法,它可以帮助团队在很短的时间内涵盖很多内容,而无需深入了解每个示例的细节。团队在制定阶段继续查看每个示例的细节,此时他们将关键示例转化为给定...何时...然后场景。

Example Mapping is an excellent technique for discovering and discussing key business rules, examples, and counterexamples at a relatively superficial level. It is very much a breadth-first approach, and it can help teams cover a lot of ground in a small amount of time, without getting into the finer details of each example. Teams go on to look at the details of each example in the Formulate phase, when they turn key examples into Given ... When ... Then scenarios.

但是,在需求发现方面,示例映射并不是我们唯一可以使用的工具。对于更复杂的需求,例如涉及工作流、用户旅程或数据转换的需求,许多团队发现另一种技术更有用。这种技术称为特征映射,我们将在下一个部分。

But when it comes to requirements discovery, Example Mapping is not the only tool at our disposal. For more complex requirements, such as ones that involve workflows, user journeys, or data transformation, many teams find another technique more useful. This technique is known as Feature Mapping, and it is what we will learn about in the next section.

6.5 特征映射

6.5 Feature Mapping

喜欢示例映射、功能映射是一种简单、低技术含量的方法,可帮助团队发现、探索和深入了解客户所需的功能。在功能映射会话中,团队将通过具体示例来说明某个功能应如何工作,并将这些示例分解为步骤和切实的业务成果。对于许多领域,更详细地映射这些示例有助于团队成员发现极端情况、消除假设并发现不确定性。

Like Example Mapping, Feature Mapping is a simple, low-tech way for teams to discover, explore, and deeply understand the features their customers need. In a Feature Mapping session, teams work through concrete examples of how a feature should work, breaking these examples down into steps and tangible business outcomes. For many domains, mapping out these examples in more detail helps team members discover edge cases, flush out assumptions, and spot uncertainty.

与示例映射一样,练习特征映射的团队使用彩色索引卡或便签,或 Miro 或 MURAL 等在线协作工具来表示关键概念。这些关键概念概述在表 6.2 中。

As with Example Mapping, teams practicing Feature Mapping use colored index cards or sticky notes, or online collaboration tools like Miro or MURAL, to represent key concepts. These key concepts are outlined in table 6.2.

表 6.2 Feature Mapping 中使用的各种卡类型

Table 6.2 The various types of cards used in Feature Mapping

卡片

Card

描述

Description

颜色

Color

例子

Example

用户故事

User story

正在讨论的功能或用户故事。功能映射既适用于高级需求(功能或史诗级别),也适用于更详细的需求。

The feature or User Story under discussion. Feature Mapping works well for both high-level requirements (at the feature or epic level) and for more detailed ones.

黄色的

Yellow

业务规则

Business rule

与用户故事相关的约束或已知验收标准。

Constraints or known acceptance criteria related to the User Story.

蓝色的

Blue

例子

Example

业务规则的具体例子和反例。

Concrete examples and counterexamples of the business rule.

绿色的

Green

Step

每个示例都分为几个步骤,并印在黄色卡片上。步骤可以表示用户执行的任务或操作,也可以表示先决条件或输入值。

Each example is broken up into steps, which go on yellow cards. Steps can represent tasks or actions a user performs, but also preconditions or input values.

黄色的

Yellow

结果

Consequence

示例的最后一步或几步代表预期的结果。我们经常在结果卡的顶部用箭头标记。

The last step or steps of an example represent the expected outcome or result. We often mark consequence cards with an arrow at the top.

淡紫色

Mauve

问题

Question

会议期间无法回答或确认的不确定性或假设。

Uncertainty or assumptions that cannot be answered or confirmed during the session.

粉色的

Pink

如果一开始觉得这很难理解,请不要担心。并非所有卡片都会在每个特征图中使用,上下文让卡片的用途变得非常清晰。颜色只是为了让地图更容易一目了然地理解而采用的惯例。在实践中,团队通常会使用手头上现有的任何颜色。现在我们已经了解了每张卡片的含义,让我们看看它们在实践中是如何工作的。

Don’t worry if this seems a lot to take in at first. Not all cards are used in every Feature Map, and the context makes the purpose of the cards quite clear. The colors are just conventions to make the map easier to understand at a glance. In practice, it is common for teams to make do with whatever colors they have on hand. Now that we have seen what each card means, let’s see how they work in practice.

6.5.1 Feature Mapping 从一个例子开始

6.5.1 Feature Mapping begins with an example

特征映射可以从功能或用户故事开始。在本例中,我们从一个相当大的功能开始;我们希望旅行者能够通过在线预订系统快速轻松地修改预订,而无需与呼叫中心的任何人交谈(图 6.12)。

Feature Mapping can start with either a feature or a User Story. In this case we start with a fairly large feature; we want travelers to be able to modify their bookings quickly and easily through the online booking system without having to talk with anyone in a call center (figure 6.12).

图 6.12 修改现有的预订用户故事

Figure 6.12 Modifying an existing booking User Story

开始任何需求发现研讨会的一个好方法是讲一个故事。产品负责人向团队介绍一个具体的、端到端的示例,说明用户如何使用此功能。在这种情况下,故事可能是这样的:

A good way to start any requirements discovery workshop is with a story. The product owner walks the team through a concrete, end-to-end example of how a user would use this feature. In this case, the story might go something like this:

塔拉(一位旅行者)预订了周一从伦敦飞往纽约的航班。但发生了一些事情,现在她不得不将行程推迟几天。她在网上查看了预订情况,决定将其改为下周三的航班。机票属于同一价格类别,因此无需额外付费。

Tara (a traveler) has booked a flight from London to New York that leaves on Monday. But something has come up, and now she has to push her trip back by a couple of days. She views her booking online and decides to modify it to fly on the following Wednesday. The ticket is in the same price category so there is no extra charge.

类似这样的故事让参与者更容易与用户产生共鸣,思考他们真正想要实现的目标。这些故事故意不详尽,因为它们旨在引发问题。如果机票不在同一价格类别会怎样?如果所有航班都满员会怎样?这些问题在研讨会中发挥着重要作用,帮助参与者探究需求并消除歧义或假设。

Stories like this make it easier for participants to relate to the user and think about what they are really trying to achieve. They are deliberately not exhaustive because they aim to provoke questions. What happens if the ticket is not in the same price category? What happens if all the flights are full? These questions play an important role in the workshop, helping participants to probe the requirement and flush out ambiguities or assumptions.

这些故事可以当场讲述,也可以由产品负责人或业务分析师提前准备。许多团队发现提前准备更容易,可以节省团队的时间,并有助于确保故事详细且精确的。

These stories can be either told on the spot or prepared ahead of time by the product owner or the business analyst. Many teams find that it is easier to prepare them in advance to save time for the team and to help ensure that the story is detailed and precise.

6.5.2 示例分为几个步骤

6.5.2 Examples are broken into steps

功能映射,我们可以先列出我们知道的业务规则,然后针对每个规则进行示例分析,就像我们在示例映射中所做的那样。或者,我们可以从一个具体的示例开始,使用步骤和结果来探索已知的业务规则和约束并发现新的规则和约束。第二种方法通常适用于涉及用户旅程的需求,因为它可以帮助人们关注更大的图景,而不会陷入特定规则的细节中。

In Feature Mapping, we can start by listing the business rules we know about and then working through examples for each of these, just as we do in Example Mapping. Alternatively, we can start with a concrete example and use the steps and outcomes to explore the known business rules and constraints and discover new ones. This second approach often works well with requirements that involve user journeys, as it helps people focus on the bigger picture and not get bogged down in the details of specific rules.

步骤是功能映射的关键部分。步骤卡可以表示先决条件或输入数据、用户旅程中的步骤、用户采取的操作或工作流程中的不同点。虽然示例可以很好地作为对话的开场白,但将示例分解为步骤可以让对话更进一步。步骤可以帮助我们发现重要的变化和极端情况,并识别系统中不同的用户旅程或信息流。

Steps are a key part of Feature Mapping. Steps cards can represent preconditions or input data, steps in the user journey, actions a user takes, or different points in a workflow. While examples make great conversation starters, breaking examples down into steps can take the conversation even further. Steps help us spot important variations and edge cases and identify different user journeys or flows of information through the system.

让我们从塔拉更改航班日期的例子开始。我们可以将此示例分为四个步骤,并将其记录在绿色示例卡旁边的黄色卡片上(图 6.13)。

Let’s start with the example where Tara changes flights to another date. We could break this example into four steps, which we record on yellow cards next to the green example card (figure 6.13).

图 6.13 示例分解为几个步骤。

Figure 6.13 Examples are broken down into steps.

但这个例子并没有结束。在特征映射中,一个示例总是有一个我们想要实现的目标或结果。或者,正如我们在特征映射术语中所说的那样,每个示例都会导致至少一个结果。这个例子的结果是 Tara 的预订将更新到新日期,无需额外费用。映射的示例现在看起来像图 6.14。

But the example doesn’t finish there. In a Feature Map, an example always has a goal or outcome that we want to achieve. Or, as we call them in Feature Mapping terms, every example leads to at least one consequence. The consequence for this example would be that Tara’s booking is updated to the new date at no additional cost. The mapped example would now look like figure 6.14.

图 6.14 每个例子都会得出一个结论。

Figure 6.14 Every example leads to a consequence.

6.5.3 寻找变化和新规则

6.5.3 Look for variations and new rules

使用以此示例为起点,团队现在寻找替代流程和结果。他们查询每个步骤,提出诸如“这里还可能发生什么?为什么这个细节很重要?我们还能期待什么其他结果?”等问题。

Using this example as a starting point, the team now looks for alternative flows and outcomes. They query each step, asking questions such as “What else could happen here? Why is this detail significant? What other outcomes might we expect?”

有些问题会引出新的业务规则和示例。例如,在这种情况下,新座位与原座位属于同一价格类别,因此没有额外费用,但如果新座位更贵怎么办?或者如果更便宜怎么办?这可能会导致另一个示例:“新航班更贵。”我们可以用一张规则卡来解释这个新示例的背景:“旅行者支付更昂贵航班的差价。”特征图现在看起来像图 6.15。

Some questions will lead to new business rules and examples. For example, in this case the new seat is in the same price category as the original one, so there is no additional cost, but what if the new seat is more expensive? Or what if it is cheaper? This could lead to another example: “The one where the new flight is more expensive.” We could explain the context of this new example with a rule card: “Traveler pays the price difference for more expensive flights.” The Feature Map would now look something like figure 6.15.

图 6.15 添加新示例

Figure 6.15 Adding new examples

第二行的箭头用来表示该部分例子中的步骤与上一行的步骤相同。

The arrow in the second row is used to indicate that the steps in this part of the example are the same as those in the previous row.

其他变化可能会导致额外的或不同的后果。例如,如果 Tara 选择乘坐更便宜的航班,她需要获得差价退款。我们可以通过添加一张额外的后果卡来强调这一需求,如下图所示图 6.16。

Other variations might lead to additional or different consequences. For example, if Tara opts for a seat on a cheaper flight, she needs to receive a refund for the difference. We can highlight this need by including an additional consequence card, as shown in figure 6.16.

图 6.16 场景可以有多个后果​​。

Figure 6.16 Scenarios can have more than one consequence.

6.5.4 寻找替代流程

6.5.4 Look for alternate flows

特征映射最擅长的事情之一是发现和讨论流程或用户旅程的变化。发现这些流程的最佳方法是将每个步骤视为“这里还可能发生什么?”例如,当 Tara 在新的旅行日查看她的座位选择时,我们可能会问的一个问题是“如果没有空座位怎么办?”在这种情况下,为了让我们的常客满意,我们需要提出退款,或者对于飞行常客会员,给予等量的飞行常客积分。这会导致我们的特征图中出现一个新行,描述不是飞行常客会员的 Tara 的情况(图 6.17)。

One of the things that Feature Mapping is very good at is discovering and discussing variations in flows or user journeys. The best way to spot these flows is to look at each step as “What else could happen here?” For example, when Tara reviews her seat options on the new day of travel, one question we might ask is “What if there are no available seats?” In this case, to keep our regular customers happy, we need to propose either a refund or, for Frequent Flyer members, a credit of an equivalent number of Frequent Flyer points. This leads to a new row in our feature map that describes the case of Tara, who is not a Frequent Flyer member (figure 6.17).

图 6.17 Tara 要求退款的地方

Figure 6.17 The one where Tara wants a refund

对于第二种情况,我们可以引入一个新角色,Fiona,她是一名常旅客会员。当她尝试将航班更改为没有可用航班的日期时,她会被建议退款或获得常旅客积分信用(图 6.18)。

For the second variation we can introduce a new persona, Fiona, who is a Frequent Flyer member. When she tries to change her flight to a day with no available flights, she is proposed either a refund or a Frequent Flyer points credit (figure 6.18).

图 6.18 Fiona 想要优惠券的地方

Figure 6.18 The one where Fiona wants a voucher

6.5.5 对相关流进行分组并记录不确定性

6.5.5 Grouping related flows and recording uncertainty

为了更大的功能图,一些团队发现按主题或更高级别的功能对业务规则进行分组很有用。例如,到目前为止,我们发现的“修改现有预订”功能的流程可以分为两类:旅行者成功修改预订,或者没有可用座位,因此旅行者获得退款。

For larger Feature Maps, some teams find it useful to group business rules by themes or higher-level functionality. For example, the flows we have discovered so far for the “modify an existing booking” feature could be grouped into two categories: the traveler successfully modifies their booking, or there are no available seats, so the traveler gets a refund.

我们可以使用蓝色规则卡直观地对流程进行分组,通常文本带有下划线,以强调这是一组相关规则的标题。与示例映射一样,我们有时希望记录不确定性或假设。问题写在粉色卡片上,其用途与示例映射中的相同。图 6.19 中可以看到这两张附加卡片的实际作用。

We can visually group flows by using blue rule cards, often with the text underlined to emphasize that this is the title of a group of related rules. As with Example Mapping, we sometimes want to record uncertainty or assumptions. Questions are written on pink cards and serve the same purpose as they do in Example Mapping. Both of these additional cards can be seen in action in figure 6.19.

图 6.19 完成的特征图

Figure 6.19 A completed feature map

功能映射适用于许多不同的领域。对于前端应用程序,团队成员可能会探索用户与应用程序交互的不同方式,以实现他们的业务目标。对于后端应用程序,他们可能会研究数据应如何在系统中流动,或者需要满足哪些不同类型的输入。

Feature Mapping works well for many different domains. For a front-end application, team members might explore the different ways users will interact with the application to achieve their business goals. For back-end applications, they may look at how data should flow through a system, or what different types of inputs need to be catered to.

功能地图不仅适用于小型、集中的用户故事。它们还可以在高层次上非常有效地用于讨论更大的功能或用户旅程。这可以帮助业务利益相关者查看和讨论大型功能或史诗需要实现的主要流程或结果。每张卡片甚至可能包含更详细的业务规则,稍后可以使用示例映射或示例表更详细地探索这些规则。

Feature Maps are not just for small, focused User Stories. They can also be used very effectively at a high level, to discuss larger features or user journeys. This can help business stakeholders see and discuss the main flows or outcomes that a large feature or epic needs to achieve. Each card might even contain more detailed business rules that can be explored later in more detail using Example Mapping or a table of examples.

正如我们将在下一章中看到的那样,Feature Maps 也很容易转换为高质量、高可读性的验收标准。这使得它们成为 BDD 新手团队或发现新事物或新方法的团队的不错选择。复杂的领域。

As we will see in the next chapter, Feature Maps are also easy to convert into high-quality, highly readable acceptance criteria. This makes them a good choice for teams new to BDD or for teams discovering a new or complex domain.

6.6 面向对象编程

6.6 OOPSI

其他BDD 家族中一个有用的协作发现实践是 OOPSI 模型。OOPSI 是“结果、输出、流程、场景、输入”的缩写,由 Jenny Martin 创建和皮特·巴克尼http://mng.bz/m2X4)。与特征映射和示例映射一样,OOPSI 是让团队在 Three Amigos 会议期间进行协作的绝佳方式,之后他们将示例转变为更正式的 Gherkin 场景。

Another useful collaborative discovery practice from the BDD family is the OOPSI model. OOPSI is short for “Outcome, Outputs, Process, Scenarios, Inputs” and was created by Jenny Martin and Pete Buckney (http://mng.bz/m2X4). Like Feature Mapping and Example Mapping, OOPSI is a great way to get teams to collaborate during Three Amigos sessions, before they turn their examples into more formal Gherkin scenarios.

在第 6.5 节中,我们看到了在特征映射中我们如何关注流程和用户旅程来了解业务规则和识别关键场景,并努力在每个流程结束时实现后果或结果。

In section 6.5, we saw how in Feature Mapping we focus on the flows and user journeys to understand business rules and identify key scenarios, working toward the consequences, or outcomes, at the end of each flow.

OOPSI 的做法则相反:我们首先确定最高价值的结果,然后逆向确定有助于实现这些结果的最高价值输出,然后找到能够说明和阐明需要做什么的例子和场景。

OOPSI does things the other way round: we start by identifying the highest value outcomes and then work backward to identify the highest value outputs that will contribute to those outcomes, and then find examples and scenarios that illustrate and clarify what needs to be done.

遵循 OOPSI 模型的 Three Amigos 会议通常涉及五个步骤:

A Three Amigos session following the OOPSI model typically involves five steps:

  1. 结果

  2. Outcome

  3. 输出

  4. Outputs

  5. 过程

  6. Process

  7. 场景

  8. Scenarios

  9. 输入

  10. Inputs

让我们在飞行常客计划应用程序中了解每个阶段的含义。

Let’s walk through what each stage means, in the context of our Frequent Flyer application.

6.6.1 结果

6.6.1 Outcomes

结果是指我们需要解决的问题,通常与 Three Amigos 会议中讨论的功能或用户故事相对应。例如,假设我们需要允许常旅客会员使用常旅客积分预订航班。用户故事可能看起来像图 6.20。

The outcome refers to the problem we need to solve and generally corresponds to the feature or User Story being discussed in the Three Amigos session. For example, suppose we need to allow Frequent Flyer members to book flights with their Frequent Flyer points. The User Story might look like the one in figure 6.20.

图 6.20 结果通常与我们想要交付的功能或用户故事相对应。

Figure 6.20 The outcome typically corresponds to the feature or User Story we want to deliver.

6.6.2 输出

6.6.2 Outputs

任何特征的价值都来自于它能产生的影响和它产生的输出。使用 OOPSI 模型,我们首先考虑特征产生的具体输出。

The value of any feature comes from the impact it can make and the outputs it produces. With the OOPSI model, we start by thinking about the concrete outputs produced by the feature.

输出既包括一切顺利时我们期望发生的情况,也包括出现问题时应该发生的情况。例如,图 6.21 中的故事的一些结果可能包括

The outputs will include both what we expect to happen when everything goes smoothly as well as what should happen when something goes wrong. For example, some of the outcomes for the story in figure 6.21 might include

  • 应签发新票。

  • The new ticket should be issued.

  • 发布“票已购买”消息。

  • A “ticket purchased” message is published.

  • 应该向常旅客发送确认电子邮件。

  • A confirmation email should be sent to the frequent flyer.

  • 常旅客的积分余额应更新。

  • The frequent flyer’s point balance should be updated.

  • 如果常旅客没有足够的积分,则应显示适当的错误消息。

  • If the frequent flyer doesn’t have enough points, an appropriate error message should be displayed.

图 6.21 结果描述的是切实可行的。

Figure 6.21 Outcomes describe what is tangible.

讨论功能的输出是深入了解基本需求的好方法,特别是当我们用一些具体的例子来说明每个输出时。客户在预订时会收到确认电子邮件吗?它是什么样的,它包含什么信息提供?

Discussing the outputs of a feature is a great way to get a deeper understanding of the underlying requirement, particularly when we illustrate each output with some concrete examples. Does the customer get a confirmation email when they book? What does it look like, and what information does it provide?

6.6.3 流程

6.6.3 Process

下一步是定义让你获得结果的过程。这部分与我们在特征映射中所做的非常相似,我们定义导致各种后果的步骤。我们通常首先从最重要的输出开始,尽管在许多情况下,相同的过程会导致许多相关的输出。例如,使用飞行常客积分预订航班的过程可能看起来像图 6.22。

The next step is to define the process that gets you to your outcomes. This part is very similar to what we do in Feature Mapping, where we define the steps that lead to various consequences. We typically start with the most important output first, though in many cases the same process will lead to a number of related outputs. For example, the process for booking a flight using Frequent Flyer points could look like the one in figure 6.22.

图 6.22 该流程规划出了实现我们想要的输出的步骤。

Figure 6.22 The process maps out the steps that lead to the outputs we want.

6.6.4 场景

6.6.4 Scenarios

一个像这样的简单过程会忽略很多边缘情况和细微变化。下一步将帮助我们识别这些情况。

A simple process like this leaves out a lot of edge cases and subtle variations. The next step helps us identify these.

在场景步骤中,团队提出了各种场景来说明流程的不同流程。在 OOPSI 中,“场景”一词既可以指业务规则或约束,也可以指需要考虑的边缘情况的示例。例如,航班上只有有限数量的座位可以用常旅客积分购买,因此当旅行者选择航班时,我们需要检查是否还有足够的合格座位。这隐含在图 6.22 中的流程图中,但通过将其记为场景,我们可以探索这种情况可能如何发生以及发生时我们应该做什么。我们可以看到一些可能的场景图 6.23。

In the Scenarios step, the team comes up with various scenarios that illustrate different flows through the process. In OOPSI, the word “scenarios” can refer to both business rules or constraints, or to examples of edge cases that need to be considered. For example, there are only a limited number of seats on a flight that can be purchased with Frequent Flyer points, so when a traveler chooses a flight, we need to check that there are still enough eligible seats available. This appears implicitly in the process diagram in figure 6.22, but by noting it down as a scenario, we can explore how this might happen and what we should do when it does. We can see some possible scenarios in figure 6.23.

图 6.23 场景说明了整个流程的不同流程以及适用的业务规则。

Figure 6.23 Scenarios illustrate different flows through the process, as well as applicable business rules.

6.6.5 输入

6.6.5 Inputs

一些我们在上一步中找到的场景是不言自明的,但通常添加一些具体的例子或数据可以使事情变得更加清晰。在 OOPSI 模型中,我们将这些具体的例子和数据称为输入

Some of the scenarios we find in the previous step will be self-explanatory, but often adding some concrete examples or data can make things a lot clearer. In the OOPSI model, we call these concrete examples and data the inputs.

表格通常是表示我们输入的一种方便格式。例如,图 6.23 中的第四个场景是一条业务规则:“可以以每美元 10 个常旅客积分的价格购买机票。”我们可以以表格形式提供一些此业务规则的示例,以及它如何与我们发现的其他场景结合使用(表 6.3)。

Tables are often a convenient format for representing our inputs. For example, the fourth scenario in figure 6.23 is a business rule: “Flights can be purchased at a rate of 10 Frequent Flyer points per dollar.” We can provide some examples of this business rule and how it works in conjunction with the other scenarios we discovered, in a tabular format (table 6.3).

表 6.3 航班购买规则示例(表格形式)

Table 6.3 Examples of flight purchase rules in a tabular format

积分余额

Point Balance

航班

Flight

成本

Cost

可用的 FF 座位

Available FF Seats

购买成功

Purchase successful

点数成本

Cost in Points

新积分平衡

New Point Balance

5,000

5,000

伦敦飞往 巴黎

London to Paris

$450

$450

是的

Yes

是的

Yes

4,500

4,500

500

500

5,000

5,000

伦敦飞往 雅典

London to Athens

$650

$650

是的

Yes

No

 

 

5,000

5,000

5,000

5,000

伦敦飞往 巴黎

London to Paris

$450

$450

No

No

 

 

5,000

5,000

图 6.24 显示了整个 OOPSI 模型,从结果和输出步骤开始,然后回到场景和输入步骤。

Figure 6.24 shows the whole OOPSI model, starting from the Outcome and Outputs steps and working back to the Scenarios and Inputs steps.

图 6.24 从结果和输出到场景和输入的完整 OOPSI 模型

Figure 6.24 A complete OOPSI model from outcomes and outputs to scenarios and inputs

概括

Summary

  • 您可以通过有关具体示例和业务规则的对话来更深入地了解用户需求。

  • You can use conversations about concrete examples and business rules to build a deeper understanding of user needs.

  • 您可以非常有效地使用表格来描述输入和预期结果的变化,并讨论可能发生的不同边缘情况。

  • You can use tables quite effectively to describe variations of inputs and expected outcomes and have conversations about different edge-cases that might occur.

  • 示例映射是一种可视化技术,使用彩色卡片来表示业务规则和示例。示例映射是快速列举和更好地理解业务规则的好方法。

  • Example Mapping is a visual technique that uses colored cards to represent business rules and examples. Example Mapping is a great way to rapidly enumerate and better understand business rules.

  • 与示例映射一样,功能映射也使用彩色卡片来表示规则和示例。与示例映射不同的是,功能映射将示例分解为步骤和结果,在更详细地探索用户旅程时非常有效。

  • Like Example Mapping, Feature Mapping also uses colored cards to represent rules and examples. Unlike Example Mapping, Feature Mapping breaks examples down into steps and outcomes and is very effective when exploring user journeys in more detail.

  • OOPSI 模型是另一种方法,它首先识别特性的关键输出,然后向后推导以识别整个过程以及边缘情况和变化的规则和示例。

  • The OOPSI model is another approach that starts by identifying key outputs of a feature and works backward to identify the overall process as well as the rules and examples of edge cases and variations.

在下一章中,你将学习如何以结构化的格式表达清晰、精确的示例,以及如何将这些示例转换为可被 Cucumber 等工具读取的可执行规范,以及规格流。

In the next chapter, you’ll learn how to express clear, precise examples in a structured format and how to turn these examples into executable specifications that can be read by tools like Cucumber and SpecFlow.


1  David A. Kolb,《体验式学习:经验是学习和发展的源泉》(Prentice Hall,1984 年)。

1  David A. Kolb, Experiential Learning: Experience as a Source of Learning and Development (Prentice Hall, 1984).

2  例如,请参阅 Dan Goodin 的“黑客剖析:即使你设置的‘复杂’密码也很容易被破解。” 《连线》,2013 年 5 月 20 日,http://www.wired.co.uk/news/archive/2013-05/28/password-cracking

2  See, for example, Dan Goodin, “Anatomy of a hack: Even your ‘complicated’ password is easy to crack.” Wired, May 20, 2013, http://www.wired.co.uk/news/archive/2013-05/28/password-cracking.

3  对于任何对这个领域感兴趣的人来说,Dan Wheeler 有一篇关于密码强度的有趣文章“zxcvbn:真实的密码强度估计”,https://tech.dropbox.com/2012/04/zxcvbn-realistic-password-strength-estimation

3  For anyone interested in this field, there’s an interesting article on password strength by Dan Wheeler, “zxcvbn: Realistic password strength estimation,” https://tech.dropbox.com/2012/04/zxcvbn-realistic-password-strength-estimation.

4  Example Mapping 最初由 Matt Wynne ( https://cucumber.io/blog/example-mapping-introduction/ )创建。

4  Example Mapping was first created by Matt Wynne (https://cucumber.io/blog/example-mapping-introduction/).

7 从示例到可执行规范

7 From examples to executable specifications

本章涵盖

This chapter covers

  • 将具体示例转化为可执行场景
  • Turning concrete examples into executable scenarios
  • 编写基本场景
  • Writing basic scenarios
  • 使用数据表来驱动场景
  • Using data tables to drive scenarios
  • 使用更多 Gherkin 关键字编写更高级的场景
  • Writing more advanced scenarios using more Gherkin keywords
  • 组织场景
  • Organizing scenarios

在上一章中,您了解了如何与利益相关者围绕业务规则和具体示例进行对话,这是建立对问题空间的共同理解的一种非常有效的方式。在本章中,您将学习如何清晰准确地表达这些示例,以便将它们转换为可执行规范和动态文档(见图 7.1)。

In the last chapter, you saw how conversations with the stakeholders around business rules and concrete examples are a very effective way to build up a common understanding of a problem space. In this chapter, you’ll learn how to express these examples clearly and precisely in a way that will allow you to transform them into executable specifications and living documentation (see figure 7.1).

图 7.1 在本章中,我们将采用前面章节中讨论和说明特性的示例,并将它们转化为可执行规范。

Figure 7.1 In this chapter we’ll take examples we used to discuss and illustrate features in previous chapters and turn them into executable specifications.

本章的目的是帮助开发人员、业务分析师、测试人员和其他感兴趣的团队成员就如何以易于自动化的方式读取和编写可执行规范达成共识。BDD 有许多明确定义的实践来实现这种共识:

The aim of this chapter is to help developers, business analysts, testers, and other interested team members get a solid shared understanding of how to read and write executable specifications in a way that makes it easy to automate them. BDD has a number of well-defined practices to achieve this shared understanding:

  • BDD 从业者将具体的例子表达为可执行场景,使用半结构化的“给定...何时...然后”格式,让利益相关者和团队成员都易于阅读。

  • BDD practitioners express concrete examples as executable scenarios, using a semi-structured Given ... When ... Then format that’s easy for both stakeholders and team members to read.

  • 此格式可以使用 BDD 工具(例如 Cucumber 或 SpecFlow)自动实现。

  • This format can be automated using BDD tools such as Cucumber or SpecFlow.

  • 表格可以用来将几个类似的示例更加简洁地组合在单一场景中,或者以更简洁的方式表达测试数据或预期结果。

  • Tables can be used to combine several similar examples more concisely in a single scenario, or to express test data or expected results in a more succinct way.

  • 经验丰富的 BDD 从业者会小心地编写他们的场景步骤,提供足够的细节以使场景有意义,但又不会太多以至于难以找到场景的基本业务目标。

  • Experienced BDD practitioners take care to write their scenario steps well, providing enough detail for the scenario to be meaningful, but not so much that the essential business goals of the scenario are hard to find.

  • 场景被组织在功能文件中,并可以用标签注释以指示跨职能关注点并协调测试执行。

  • Scenarios are organized in feature files and can be annotated with tags to indicate cross-functional concerns and to coordinate test execution.

每个人都必须熟悉这些场景中使用的符号和结构;这样,团队成员就可以专注于讨论需求,而不会被你用来表达需求的形式所分散注意力。和任何语言一样,有一些常见的模式和结构会反复出现(可以说是习语),这些模式和结构可以帮助你更流畅地表达你的想法。

It’s important for everyone to be comfortable with the notation and structures used for these scenarios; that way, team members can focus on discussing the requirements and not be distracted by the form that you use to express them. As in any language, there are common patterns and structures that recur (idioms, so to speak), and those can help you express your ideas more fluently.

在下一章中,您将了解如何使用 Cucumber 在 Java 中自动执行这些示例。如果您想尝试我们在本章中讨论的示例,可以从 GitHub(https://github.com/bdd-in-action/second-edition/)或 Manning 网站下载源代码。

In the next chapter, you’ll see how to automate these examples in Java using Cucumber. If you want to experiment with the examples we discuss in this chapter, you can download the source code from GitHub (https://github.com/bdd-in-action/second-edition/) or the Manning website.

在本章的其余部分,我们将从第 5 章中断的地方继续,并使用通过表格、示例映射和特征映射发现的一些示例。

In the rest of the chapter, we’ll pick up where we left off in chapter 5 and use some of the examples we discovered using tables, Example Mapping, and Feature Mapping.

7.1 将具体示例转化为可执行场景

7.1 Turning concrete examples into executable scenarios

BDD 背后的核心概念之一是,您可以用利益相关者可读且可作为自动化测试套件的一部分执行的形式来表达重要的具体示例。您将用用户的母语编写可执行规范,并生成测试结果,这些结果不是根据类和方法报告成功或失败,而是根据利益相关者要求的功能报告成功或失败。利益相关者将能够看到他们自己的话出现在动态文档中,这极大地增强了他们对您了解其问题的信心。这就是 Cucumber 等 BDD 工具所带来的好处。

One of the core concepts behind BDD is the idea that you can express significant concrete examples in a form that’s both readable for stakeholders and executable as part of your automated test suite. You’ll write executable specifications in the native language of your users and produce test results that report success or failure not in terms of classes and methods, but in terms of the features that the stakeholders requested. Stakeholders will be able to see their own words appear in the living documentation, which does wonders in increasing their confidence that you’ve understood their problems. This is what BDD tools like Cucumber bring to the table.

当您使用此类 BDD 工具自动执行验收标准时,您会以稍微更结构化的形式(通常称为场景)表达示例。Dan North 在 2000 年代中期为这些场景定义了一种规范形式,该形式基于简单的 Given ... When ... Then 结构构建,自此以后,这种格式一直被 BDD 从业者广泛采用。

When you automate your acceptance criteria using this sort of BDD tool, you express your examples in a slightly more structured form, often referred to as scenarios. Dan North defined a canonical form for these scenarios in the mid-2000s, built around a simple Given ... When ... Then structure, and this format has been widely adopted by BDD practitioners ever since.

在上一章中,我们探讨了“通过航班赚取飞行常客积分”故事的业务规则和示例。您可以在图 7.2 中的示例图中看到一些规则和示例。

In the previous chapter, we explored business rules and examples for the “Earning Frequent Flyer points from flights” story. You can see some of the rules and examples in the Example Map in figure 7.2.

图 7.2 “赚取飞行常客积分的用户故事”

Figure 7.2 The “Earning Frequent Flyer points User Story”

我们在此图中考虑的第一个业务规则是“欧洲境内航班可赚取 100 点积分”。我们找到的示例是“Tara 从巴黎飞往柏林的经济舱”。您可以按如下所示编写此示例:1

The first business rule we considered in this map is “Flights within Europe earn 100 points.” The example we found was “The one where Tara flies economy from Paris to Berlin.” You could write this example as shown:1

      特色:通过航班赚取飞行常客积分
 
        场景:欧洲境内航班可赚取 100 积分
          鉴于Tara 是常旅客
          她完成巴黎和柏林之间的飞行后
          那么她应该获得 100 分
      Feature: Earning Frequent Flyer points from flights
 
        Scenario: Flights within Europe earn 100 points
          Given Tara is a Frequent Flyer traveler
          When she completes a flight between Paris and Berlin
          Then she should earn 100 points

尽管此示例比自由文本段落风格化程度略高,但您仍使用利益相关者的语言。经过一些练习,利益相关者很快就会熟悉此格式,能够提出并讨论类似此示例。

Although the example is a little more stylized than a free-text paragraph, you’re still speaking the language of the stakeholders. With a little practice, stakeholders quickly become comfortable enough with the format to be able to propose and discuss examples like this one.

当利益相关者的母语不是英语时,这种方法同样有效;你可以用任何语言编写这样的场景。例如,以下是该场景的法语版本:

This approach works equally well when the native language of the stakeholders is not English; you can write scenarios like this in any language. For example, here’s the equivalent of this scenario in French:

#语言:fr
功能: 飞行常客积分获取和完整列表
 
  场景:Les vols en Europe 价格 100 分
    塔拉是飞行常客计划的
会员    巴黎和柏林之间的
故事    阿洛斯·艾丽·德维特·加格纳 100 分
#language: fr
Fonctionnalité: Gagner des points Frequent Flyer en completant des vols
 
  Scénario: Les vols en Europe valent 100 points
    Etant donné que Tara est un member du programme Frequent Flyer
    Quand elle complete un vol entre Paris and Berlin
    Alors elle devrait gagner 100 points

此类场景不仅对非技术人员来说易于理解,而且也是可执行的。Cucumber 和 SpecFlow 等 BDD 工具可以读取和执行这些场景,以验证应用程序的行为并生成有意义的测试报告。这些测试报告是动态文档的核心部分,可帮助您理解和维护应用程序(见图 7.3)。

Scenarios like this are not only readable for nontechnical folk; they are also executable. BDD tools like Cucumber and SpecFlow can read and execute these scenarios to verify your application’s behavior and generate meaningful test reports. These test reports are a central part of the living documentation that will help you understand and maintain the application (see figure 7.3).

图 7.3 当示例以场景表达时,它们可以被自动化并用于生成动态文档。

Figure 7.3 When examples are expressed as scenarios, they can be automated and used to generate living documentation.

我们将在第 8 章中介绍如何利用这种自动化功能。但首先,您需要学习如何使用 Cucumber 等工具以这种格式编写有效的场景。

We’ll look at how you can take advantage of this sort of automation in chapter 8. But first, you need to learn how to write effective scenarios in this format using tools like Cucumber.

7.2 编写可执行场景

7.2 Writing executable scenarios

场景以这种格式编写的代码将构成可执行规范的核心。但要使它们真正可执行,您需要将它们集成到您的项目中。

Scenarios written in this format will make up the core of your executable specifications. But to make them truly executable, you need to integrate them into your projects.

在本节中,您将了解如何使用 Gherkin 实现此目的。Gherkin 是 Cucumber 和绝大多数基于 Cucumber 的 BDD 工具使用的语言,包括 SpecFlow(适用于 .NET)、Behave(适用于 Python)等。另一个较旧的基于 Java 的工具 JBehave 也使用非常相似的格式。

In this section, you’ll see how to do this using Gherkin. Gherkin is the language used by Cucumber and the vast majority of Cucumber-based BDD tools, including SpecFlow (for .NET), Behave (for Python), and many others. Another older Java-based tool, JBehave, also uses a very similar format.

场景存储在简单的文本文件中,并按功能分组。这些文件在逻辑上被称为功能文件

Scenarios are stored in simple text files and grouped by feature. These files are called, logically enough, feature files.

7.2.1 特征文件有标题和描述

7.2.1 A feature file has a title and a description

功能文件的顶部是您可以包含相应功能描述的部分。典型示例如下所示:

At the top of a feature file is a section where you can include the description of the corresponding feature. A typical example is shown here:

特色:通过航班赚取飞行常客积分                      
  为了提高顾客忠诚度                                   
  作为航空公司销售经理                                            
  我希望旅客在搭乘我们的航班时能够赚取飞行常客积分   
 
  场景:欧洲境内航班可赚取 100 积分                        
    鉴于Tara 是常旅客                              
    她完成巴黎和柏林之间的飞行                 时
    那么她应该获得 100 分                                      
 
  场景:欧洲以外的航班每飞行 10 公里可赚取 1 点积分              
    假设伦敦到纽约的距离是 5500 公里                
    Tara是一名常旅客                                
    她完成伦敦和纽约之间的飞行              后
    那么她应该获得 550 分                                      
Feature: Earning Frequent Flyer points from flights                      
  In order to improve customer loyalty                                   
  As an airline sales manager                                            
  I want travelers to earn frequent flyer points when they fly with us   
 
  Scenario: Flights within Europe earn 100 points                        
    Given Tara is a Frequent Flyer traveler                              
    When she completes a flight between Paris and Berlin                 
    Then she should earn 100 points                                      
 
  Scenario: Flights outside Europe earn 1 point every 10 km              
    Given the distance from London to New York is 5500 km                
    And Tara is a Frequent Flyer traveler                                
    When she completes a flight between London and New York              
    Then she should earn 550 points                                      

标题后面可以跟随着功能的简短描述。

A short description of the feature can follow the title.

在 Gherkin 中使用 Feature 关键字来表示功能标题。

In Gherkin use the Feature keyword to indicate a feature title.

接下来是一种或多种情况。

One or more scenarios follow.

关键词Feature标记功能的标题。Dan North 建议标题应该描述用户或利益相关者想要执行的活动。2这使得工作更容易包含,范围更容易确定。例如,“通过航班赚取飞行常客积分”是一个相对明确的用户活动;实施后,飞行常客会员将能够在飞行时赚取积分。另一方面,“飞行常客积分管理”可能还包括在飞行常客会员在合作公司购物时奖励他们积分,让会员查看他们当前的积分状态,等等。

The Feature keyword marks the feature’s title. Dan North suggests that the title should describe an activity that a user or stakeholder would like to perform.2 This makes the work easier to contain and the scope easier to nail down. For example, “Earning Frequent Flyer points from flights” is a relatively well-defined user activity; when it’s implemented, Frequent Flyer members will be able to earn points when they fly. On the other hand, “Frequent Flyer point management” might also include rewarding Frequent Flyer members points when they make purchases with partner companies, letting members view their current point status, and so forth.

除了标题之外,最好还包含功能的简短描述,以便读者了解文件所含场景背后的基本业务目标和背景。此标题和第一个场景之间的任何文本都将被视为功能描述。

In addition to the title, it’s a good idea to include a short description of your feature so that readers can understand the underlying business objectives and background behind the scenarios that the file contains. Any text between this title and the first scenario is treated as a feature description.

如果您使用敏捷软件管理工具或问题跟踪系统(如 JIRA)以电子方式存储功能描述,则可以配置报告工具(如 Serenity BDD)以从这些系统中获取此信息并将其显示在测试报告中(您将在第 16 章中看到如何执行此操作)。

If you store feature descriptions electronically, using Agile software management tools or even an issue tracking system such as JIRA, you can configure reporting tools such as Serenity BDD to fetch this information from these systems and display it in the test reports (you’ll see how to do this in chapter 16).

7.2.2 描述场景

7.2.2 Describing the scenarios

功能标题和描述,功能文件描述了一组行为示例,我们称之为场景。每个场景都以Scenario关键字开头以及描述性标题:

After the feature title and description, a feature file describes a set of examples of behavior, which we call scenarios. Each scenario starts with the Scenario keyword and a descriptive title:

场景:<a title>
Scenario: <a title>

标题很重要。与 BDD 中的大多数事情一样,良好的沟通至关重要。场景标题应该用一个简短的陈述句概括此示例的特别之处,有点像书的副标题。您可以在图 7.4 中看到一些示例。

The title is important. As with most things in BDD, good communication is essential. The scenario title should summarize what is special about this example in a short, declarative sentence, a bit like a subtitle for a book. You can see some examples in figure 7.4.

图 7.4 使用 Serenity BDD 呈现的 Cucumber 报告,展示场景标题如何在动态文档报告中充当副标题。

Figure 7.4 A Cucumber report rendered using Serenity BDD, showing how scenario titles act as subheadings in the living documentation reports.

场景标题在报告中起着关键作用,使动态文档报告更易于阅读和浏览。拥有简洁的场景标题列表可以更轻松地理解特定功能应该做什么,而无需研究给定...何时...然后文本的详细信息。当测试中断时,它还可以更轻松地隔离问题。

Scenario titles play a key role in reporting, making the living documentation reports easier to read and navigate. Having a succinct list of scenario titles makes it easier to understand what a particular feature is supposed to do, without having to study the details of the Given ... When ... Then text. It also makes it easier to isolate issues when tests break.

Matt Wynne 3建议的另一个好做法是在标题中总结场景的“给定”和“何时”部分,并避免包含任何预期结果。由于场景基于真实的业务示例,因此背景和事件通常相对稳定,但预期结果可能会随着组织业务方式的变化和发展而发生变化。

Another good practice suggested by Matt Wynne3 is to summarize the Given and When sections of the scenario in the title and avoid including any expected outcomes. Because scenarios are based on real business examples, the context and events are usually relatively stable, but the expected outcomes may change as the organization changes and evolves the way it does business.

Gherkin 还允许您用描述来补充场景标题,如本例所示:

Gherkin also lets you complement the scenario title with a description, as shown in this example:

场景:欧洲境外的航班根据飞行距离赚取积分
 
普通航班每飞行 10 公里可赚取 1 个积分           
 
  假设伦敦到纽约的距离是 5500 公里
  Tara是一名常旅客
  她完成伦敦和纽约之间的飞行
后  那么她应该获得 550 分
Scenario: Flights outside Europe earn points based on distance traveled
 
Normal flights earn 1 point every 10 km           
 
  Given the distance from London to New York is 5500 km
  And Tara is a Frequent Flyer traveler
  When she completes a flight between London and New York
  Then she should earn 550 points

场景标题之后和第一个“给出”之前的任何内容都被视为描述。

Anything after the scenario title and before the first Given is considered a description.

这是添加有关业务规则或计算的额外详细信息的好方法,因为附加文本将作为生活的一部分出现文档。

This is a great way to add extra details about business rules or calculations, as the additional text will appear as part of the living documentation.

7.2.3 给定...当...然后结构

7.2.3 The Given ... When ... Then structure

每个场景的核心由三部分组成:初始状态或上下文、动作或事件以及预期结果。正如您在第 1 章和第 2 章中看到的那样,这些内容使用以下结构来表达:

The meat of each scenario is made up of three parts: an initial state or context, an action or event, and an expected result. As you saw in chapters 1 and 2, these are expressed using the following structure:

给定一个                上下文
<某事发生>          
然后<我们期待一些结果>     
Given <a context>                
When <something happens>         
Then <we expect some outcome>    

先决条件和/或输入

Preconditions and/or inputs

测试的动作

The action under test

预期成果

The expected outcome

这是一种简单但用途广泛的格式。它可以帮助您清晰地定义测试上下文、正在测试的操作以及预期结果。它还可以帮助您专注于需求旨在实现的目标,而不是如何实现。让我们更详细地了解每个步骤。

This is a simple yet surprisingly versatile format. It helps you cleanly define the context of a test, what action is being tested, and what the expected outcome should be. It also helps you focus on what the requirement aims to achieve, rather than on how it will do so. Let’s look at each of these steps in more detail.

Given 奠定了基础

Given sets the stage

Given步骤描述了测试的前提条件。它设置测试所需的任何测试数据,并通常将应用程序置于正确的预测试状态。通常,这包括创建任何所需的测试数据,或者对于 Web 应用程序,登录并导航到正确的页面。有时,Given即使在测试实施中不需要采取任何行动,步骤也可能纯粹是提供信息,以提供一些上下文或背景。

The Given step describes the preconditions for your test. It sets up any test data your test needs and generally puts the application in the correct pretest state. Typically, this includes things such as creating any required test data, or, for a web application, logging on and navigating to the right page. Sometimes a Given step may be purely informative, to provide some context or background, even if no action is required in the test implementation.

您应该小心,只包含与场景直接相关的先决条件。过多的Givens 会使读者更难准确了解场景运行所需的条件,并使场景更难维护。但是,Given步骤中应该存在但没有存在的先决条件实际上是可能导致以后产生误解的假设。

You should be careful to only include the preconditions that are directly related to the scenario. Too many Givens can make it harder for a reader to know precisely what’s required for the scenario to work and can make the scenarios harder to maintain. But preconditions that should be present in the Given steps, but aren’t, are effectively assumptions that can lead to misunderstandings later on.

例如,假设我们想要自动化我们在第 6 章中发现的场景,其中旅行者修改其预订日期(见图 7.5)。

For example, suppose we want to automate the scenario we discovered in chapter 6, where a traveler modifies the date of their booking (see figure 7.5).

图 7.5 显示旅行者更改预订的特征图

Figure 7.5 A Feature Map showing a traveler changing their booking

我们可以像这样编写这个场景:

We could write this scenario as shown:

场景:将航班改期
  鉴于Tara 已预订以下航班:
    | 出发日期 | 目的地 | 日期 | 舱位 |
    | 伦敦 | 纽约 | 2020-01-13 | 经济 |
  Tara 将航班日期更新为 2020 年 1 月 15 日时
  那么就不应该有额外收费
  并且预订应更新为以下内容:
    | 出发日期 | 目的地 | 日期 | 舱位 |
    | 伦敦 | 纽约 | 2020-01-15 | 经济 |
Scenario: Change a flight to another date
  Given Tara has booked the following flight:
    | Departure | Destination | Date       | Class   |
    | London    | New York    | 13-01-2020 | Economy |
  When Tara updates the flight date to 15-01-2020
  Then there should be no additional charge
  And the booking should be updated to the following:
    | Departure | Destination | Date       | Class   |
    | London    | New York    | 15-01-2020 | Economy |

但是,我们缺少一些关于 Tara 的背景信息。如果此功能仅适用于常旅客会员(非常旅客会员可能需要支付费用才能更改预订),那么在步骤中提及这一事实很重要Given

However, we are missing some context about who Tara is. If this feature is only applicable for Frequent Flyer members (non-Frequent Flyer members might be charged a fee to change a booking), then it would be important to mention this fact in the Given steps:

场景:将航班改期
  鉴于Tara 是常旅客
  Tara 已预订以下航班:
    | 出发日期 | 目的地 | 日期 | 舱位 |
    | 伦敦 | 纽约 | 2020-01-13 | 经济 |
  ...
Scenario: Change a flight to another date
  Given Tara is a Frequent Flyer traveler
  And Tara has booked the following flight:
    | Departure | Destination | Date       | Class   |
    | London    | New York    | 13-01-2020 | Economy |
  ...

其他先决条件可能不那么有用。例如,假设要更改她的预订,Tara 首先需要登录常旅客应用程序。我们可以将此作为给定步骤之一。但是,它与场景中的业务逻辑没有直接关系。此外,如果 Tara 是常旅客,那么我们可以放心地假设她需要登录才能更改航班,因此场景中显示的第二个给定步骤不应该是包括:

Other preconditions might not be so useful. For example, suppose that, to change her booking, Tara first needs to log on to the Frequent Flyer application. We could include this as one of the given steps. However, it is not directly related to the business logic in the scenario. Furthermore, if Tara is a frequent flyer, then we can safely assume that she needs to log on to be able to change her flights, so the second given shown in the scenario should not be included:

场景:将航班改期
  鉴于 Tara 是常旅客                                  
  Tara 已使用 tara@email.com 登录飞行常客计划应用程序    
  Tara 已预订以下航班:
    | 出发日期 | 目的地 | 日期 | 舱位 |
    | 伦敦 | 纽约 | 2020-01-13 | 经济 |
Scenario: Change a flight to another date
  Given Tara is a Frequent Flyer traveler                                  
  And Tara has logged on to the Frequent Flyer app using tara@email.com    
  And Tara has booked the following flight:
    | Departure | Destination | Date       | Class   |
    | London    | New York    | 13-01-2020 | Economy |

这个细节对于场景中的业务逻辑很重要。

This detail is important to the business logic in the scenario.

此步骤是实现细节,与场景结果无直接关系。

This step is an implementation detail that is not directly related to the scenario outcome.

何时包含待测试的动作

When contains the action under test

When步骤描述了您要测试的主要操作或事件。这可能是用户在网站上执行某些操作,也可能是其他非 UI 事件,例如处理交易或处理事件消息。此操作将生成一些可观察到的结果,您将在Then步骤中验证这些结果。

The When step describes the principal action or event that you want to test. This could be a user performing some action on a website, or some other non-UI event, such as processing a transaction or handling an event message. This action will generate some observed outcome, which you’ll verify in the Then step.

然后描述预期结果

Then describes the expected outcomes

Then步骤将观察到的结果或系统状态与您的预期进行比较。结果应该与您期望从故事或功能中获得的业务价值相关联。属于到。

The Then step compares the observed outcome or state of the system with what you expect. The outcome should tie back to the business value you expect to get out of the story or feature this scenario belongs to.

7.2.4 并且和但是

7.2.4 Ands and buts

任何上述步骤可以使用 进行扩展and。Gherkin 还允许您使用同义词but。你之前肯定见过这个:

Any of the previous steps can be extended using and. Gherkin also allows you to use the synonym but. You’ve seen this before:

场景:欧洲境外的航班根据飞行距离赚取积分
  假设伦敦到纽约的距离是 5500 公里
  Tara是一名常旅客                     
  她完成伦敦和纽约之间的飞行
后  那么她应该获得 550 分
Scenario: Flights outside Europe earn points based on distance traveled
  Given the distance from London to New York is 5500 km
  And Tara is a Frequent Flyer traveler                     
  When she completes a flight between London and New York
  Then she should earn 550 points

And 等同于 Given。

And is equivalent to Given.

从技术上讲,BDD 工具认为任何带有And或 的步骤都与上一步不带有或 的But步骤相同。主要目标是使场景更容易阅读。AndBut

Technically, the BDD tools consider any step with And or But to be the same as the previous step that wasn’t And or But. The main goal is to make the scenarios read more easily.

保持 Given ... When ... Then 子句简洁而有针对性通常是一个好习惯。如果您想将两个条件放在同一个步骤中,请考虑将它们拆分为两个单独的步骤。这将使场景更易于阅读,并让开发人员更自由地在场景之间重复使用步骤。

It’s often a good habit to keep Given ... When ... Then clauses concise and focused. If you’re tempted to place two conditions in the same step, consider splitting them into two separate steps. This will make the scenario easier to read and give developers more freedom to reuse steps between scenarios.

例如,假设旅客在特殊奖励飞行期间飞行可获得奖励积分。表达这一点的一种方式可能是:

For example, suppose travelers can earn bonus points if they fly during special bonus-flyer periods. One way to express this might be the following:

场景:在奖励飞行期内赚取额外积分
  
  欧洲航班在奖励飞行期间可额外赚取 50 点积分
  
  鉴于Tara 是常旅客
  她在“Bonus Flyer”期间从伦敦飞往巴黎时
  那么她应该获得 100 分和 50 分奖励积分            
Scenario: Earning extra points in a bonus flyer period
  
  European flights earn 50 extra points during bonus flyer periods
  
  Given Tara is a Frequent Flyer traveler
  When she flies from London to Paris in a 'Bonus Flyer' period
  Then she should earn 100 points and 50 bonus points            

复合 Then 步骤

A composite Then step

或者,你可以将GivenThen步骤拆分为更小的步骤:

Alternatively, you could split the Given and Then steps into smaller ones:

场景:在奖励飞行期内赚取额外积分
  
  欧洲航班在奖励飞行期间可额外赚取 50 点积分
  
  鉴于Tara 是常旅客
  她在“Bonus Flyer”期间从伦敦飞往巴黎时
  那么她应该获得 100 分                
  应该获得 50 分奖励            
Scenario: Earning extra points in a bonus flyer period
  
  European flights earn 50 extra points during bonus flyer periods
  
  Given Tara is a Frequent Flyer traveler
  When she flies from London to Paris in a 'Bonus Flyer' period
  Then she should earn 100 points                
  And she should earn 50 bonus points            

此步骤可重复使用。

This step can be reused.

这个也可以。

So can this one.

虽然第二版稍长一些,但它有几个优点。每个步骤都侧重于问题的一个特定方面——如果出现问题或需求发生变化,将更容易看到需要更改的内容。此外,重用也更容易。步骤 1 和 2 也用于您见过的其他一些场景,因此您可以通过重用来简化测试的编写和维护他们。

Although it’s slightly longer, this second version has several advantages. Each step is focused on a particular aspect of the problem—if something breaks, or the requirements change, it will be easier to see what needs to be changed. In addition, reuse is easier. Steps 1 and 2 are also used in some of the other scenarios you’ve seen, so you can simplify writing and maintaining the tests by reusing them.

7.2.5 注释

7.2.5 Comments

可能偶尔还想在您的功能文件中添加评论,例如记录有关如何实现场景的一些技术细节。

You may also occasionally want to place comments in your feature files, such as to note some technical detail about how the scenario should be implemented.

#您可以在行首放置井号 ( ) 来插入注释或注释掉某一行:

You can insert a comment, or comment out a line, by placing the hash character (#) at the start of a line:

#这个功能对于营销团队来说非常重要      
特色:通过航班赚取飞行常客积分
  为了提高顾客忠诚度
  作为航空公司销售经理
  我希望旅客在搭乘我们的航班时能够赚取飞行常客积分
  场景:欧洲境内航班可赚取 100 积分
    #鉴于Tara 是常旅客                 
    她完成巴黎和柏林之间的飞行后
    那么她应该获得 100 分
# This feature is really important for the Marketing team     
Feature: Earning Frequent Flyer points from flights
  In order to improve customer loyalty
  As an airline sales manager
  I want travelers to earn frequent flyer points when they fly with us
  Scenario: Flights within Europe earn 100 points
    # Given Tara is a Frequent Flyer traveler                 
    When she completes a flight between Paris and Berlin
    Then she should earn 100 points

Gherkin 中的注释行以 # 字符开头。它可用于注释掉任何行。

A comment line in Gherkin starts with the # character. It can be used to comment out any line.

通常用于注释掉一些步骤。

It’s often used to comment out steps.

注释可以出现在场景中的任何位置,但它们通常用于为其他开发人员留下技术说明或暂时注释掉某个步骤。

Comments can appear anywhere in the scenario, though they’re often used to leave a technical note for other developers or to temporarily comment out a step.

与您之前看到的叙述和描述性文本不同,注释不是动态文档的一部分。它们不会出现在报告中,因此对利益相关者的沟通价值有限。因此,除了暂时注释掉某个步骤外,注释还应该用于适度。

Unlike the narrative and descriptive texts you saw earlier, comments are not part of the living documentation. They don’t appear in the reports and are therefore of limited communication value for the stakeholders. For this reason, other than for temporarily commenting out a step, comments should be used with moderation.

7.3 在场景中使用表格

7.3 Using tables in scenarios

场景就像应用程序代码一样:编写时应以易于阅读和维护为目的。如果您将场景用作动态文档,它们可能会比开发项目持续更长时间,因此确保它们易于理解和在将来更新非常重要。

Scenarios are like application code: you should write with the intention of making readability and maintenance easy. If you’re using your scenarios as living documentation, they’ll likely long outlast the development project, and it’s important to make sure that they’re easy to understand and update in the future.

避免这种情况的一种方法是将大量重复的文本包含在场景中。在编程中,重复是可维护代码的最大敌人之一,对于场景也是如此。但是,如果您只使用我们迄今为止讨论过的 Given ... When ... Then 符号,有时很难避免场景过于冗长,充斥着重复的文本。在下一节中,我们将介绍如何以不同的方式使用表格来避免重复、提高可读性并简化维护。

One way not to do this is to include a lot of duplicated text in your scenarios. In programming, duplication is one of the worst enemies of maintainable code, and the same applies to scenarios. But if you only use the Given ... When ... Then notation we’ve discussed so far, it’s sometimes hard to avoid overly wordy scenarios peppered with duplicated text. In the following section, we’ll look at how you can use tables in different ways to avoid duplication, improve readability, and make maintenance easier.

7.3.1 在各个步骤中使用表格

7.3.1 Using tables in individual steps

认为您正在开发一项功能,允许常旅客会员将积分转让给其他会员。例如,假设 Danielle 和 Martin 是同一个家庭中的常旅客会员。Danielle 和 Martin 在过去一年中都积累了很多积分。他们想用积分去度假,但他们都没有足够的积分在一次预订中直接购买机票。Martin 需要能够将他的部分积分转让给 Danielle,以便她可以用她的积分为他们两人购买机票。

Suppose you’re working on a feature that allows Frequent Flyer members to transfer points to other members. For example, suppose Danielle and Martin are Frequent Flyer members in the same family. Both Danielle and Martin have accumulated a lot of points over the year. They want to go on holidays using their points, but neither of them has enough points to buy the tickets outright in a single booking. Martin needs to be able to transfer some of his points to Danielle so that she can purchase the flights for both of them with her points.

您可以按如下方式表达这种情况:

You could express this scenario as follows:

场景:家庭成员之间转移积分
鉴于丹妮尔和马丁是家庭成员
Danielle 的账户有 100000 积分和 800 状态积分     
马丁的账户有 50000 积分和 50 状态点         
马丁将 40,000 积分转给丹妮尔
时那么马丁应该有 10000 分和 50 状态分          
Danielle 应该有 140000 分和 800 状态分       
Scenario: Transfer points between family members
Given Danielle and Martin are family members
And Danielle's account has 100000 points and 800 status points     
And Martin's account has 50000 points and 50 status points         
When Martin transfers 40000 points to Danielle
Then Martin should have 10000 points and 50 status points          
And Danielle should have 140000 points and 800 status points       

这里有很多重复

A lot of duplication here

这里也是

Here too

问题在于,这种情况下存在大量重复和混乱,所有单词的含义都丢失了。编写此场景的更好方法是以更简洁的表格格式表达数据,如下所示:

The problem is that there’s a lot of repetition and clutter in this scenario, and the meaning gets lost in all the words. A much better way to write this scenario would be to express the data in a more concise tabular format, like this:

场景:家庭成员之间转移积分
    鉴于丹妮尔和马丁是家庭成员
    他们有以下账户:
      | 所有者 | 积分 | 状态积分 |             
      | 丹妮尔 | 100000 | 800 |             
      | 马丁 | 50000 | 50 |             
    马丁将 40,000 积分转给丹妮尔
时      | 所有者 | 积分 | 状态积分 |             
      | 丹妮尔 | 140000 | 800 |             
      | 马丁 | 10000 | 50 |             
Scenario: Transfer points between family members
    Given Danielle and Martin are family members
    And they have the following accounts:
      | owner     | points | status points |            
      | Danielle  | 100000 | 800           |            
      | Martin    | 50000  | 50            |            
    When Martin transfers 40000 points to Danielle
      | owner    | points | status points  |            
      | Danielle | 140000 | 800            |            
      | Martin   | 10000  | 50             |            

为Given步骤提供数据表。

Provide a table of data for the Given step.

Then 步骤的数据

Data for the Then step

表格直接在GivenThen步骤之后开始,其中的值用竖线 ( |) 分隔。在这种情况下,每列顶部的标题很有用。

The tables start directly after the Given and Then steps, with the values separated by pipes (|). The headers at the top of each column are useful in this case.

此类表格是一种简洁有效的表示类似数据集的方法,例如此处显示的用户帐户。表格还可用于表示单个记录,而不是记录集合,方法是在第一列中垂直组织字段名称,然后将相应的值放在第二列中。您可以在此处看到这种方法的一个示例,其中我们表示一个特定用户的帐户详细信息:

Tables like this are a concise and effective way to represent sets of similar data, such as the user accounts shown here. Tables can also be used to represent a single record, rather than a collection of records, by organizing the field names vertically in the first column and then placing the corresponding values in the second column. You can see an example of this approach here, where we represent the account details of one particular user:

鉴于Daniel 是一名常旅客,其详细信息如下:
  | 家乡城市 | 伦敦 |
  | 积分 | 100000 |
  | 状态点 | 50 |   
Given Daniel is a Frequent Flyer with the following details:
  | Home City     | London |
  | Points        | 100000 |
  | Status Points | 50     |   

您甚至可以使用数据表来表示简单的值列表,如下所示:

You can even use data tables to represent a simple list of values, as shown here:

那么Tara 应该能够升级到以下舱位之一:
| 高级经济舱 |
| 商业 |
| 第一页 |
Then Tara should be able to upgrade to one of the following cabin classes:
| Premium Economy |
| Business        |
| First           |

嵌入表格数据是一种以清晰简洁的方式表达先决条件和预期结果的好方法。但在您的场景中,还有另一种同样有用的使用表格数据的方法:例子。

Embedding tabular data is a great way to express preconditions and expected outcomes in a clear and concise way. But there’s another equally useful way to use tabular data in your scenarios: tables of examples.

7.3.2 使用示例表

7.3.2 Using tables of examples

之前中,我们看到 Bianca 和她的团队使用表格来更好地理解如何将过去的航班计入新加入者的飞行常客积分。表格如下所示:

Previously, we saw how Bianca and her team used a table to better understand how past flights could be counted toward the Frequent Flyer points of a new joiner. The table looked like this:

航班

Flight Number

航班日期

Flight Date

地位

Status

有资格的

Eligible

原因

Reason

FH-99

FH-99

60天前

60 days ago

完全的

COMPLETED

是的

Yes

 

 

FH-87

FH-87

100天前

100 days ago

完全的

COMPLETED

No

太旧

Too old

OH-101

OH-101

60天前

60 days ago

完全的

COMPLETED

No

并非高空飞行

Not a Flying High flight

FH-99

FH-99

60天前

60 days ago

取消

CANCELLED

No

必须完成

Must be completed

FH-99

FH-99

5天后

In 5 days time

确认的

CONFIRMED

No

必定发生

Must have taken place

您可以编写示例来将这些规则作为单独的场景进行说明,如下面的清单所示。

You could write examples illustrating these rules as separate scenarios, as shown in the following listing.

清单 7.1 计算过往航班合格性的场景

Listing 7.1 Scenarios for calculating the eligibility of past flights

特色:通过过往航班赚取积分
 
  作为Flying High 常旅客计划经理
  我希望新会员能获得近期航班的积分
  以便更多人加入该计划
  
场景:符合条件的航班
  鉴于Todd 刚刚加入飞行常客计划
  托德要求将以下航班记入他的账户:
    | 航班号 | 航班日期 | 状态 |
    | FH-99 | 60天前 | 已完成 |
  那么该航班应被视为符合条件
 
场景:旧航班
  鉴于Todd 刚刚加入飞行常客计划
  托德要求将以下航班记入他的账户:
    | 航班号 | 航班日期 | 状态 |
    | FH-87 | 100天前 | 已完成 |
  那么该航班应被视为不符合条件
 
场景:不是高空飞行
  鉴于Todd 刚刚加入飞行常客计划
  托德要求将以下航班记入他的账户:
    | 航班号 | 航班日期 | 状态 |
    | OH-101 | 60天前 | 已完成 |
  那么该航班应被视为不符合条件
 
场景:必须完成航班
  鉴于Todd 刚刚加入飞行常客计划
  托德要求将以下航班记入他的账户:
    | 航班号 | 航班日期 | 状态 |
    | FH-99 | 60天前 | 已取消 |
  那么该航班应被视为不符合条件
 
场景:航班必须已经起飞
  鉴于Todd 刚刚加入飞行常客计划
  托德要求将以下航班记入他的账户:
    | 航班号 | 航班日期 | 状态 |
    | FH-99 | 5天后 | 已确认 |
  那么该航班应被视为不符合条件
Feature: Earning points from past flights
 
  As a Flying High Frequent Flyer program manager
  I want new members to be credited with points from their recent flights
  So that more people will join the program
  
Scenario: Eligible flight
  Given Todd has just joined the Frequent Flyer program
  And Todd asks for the following flight to be credited to his account:
    | Flight Number | Flight Date | Status    |
    | FH-99         | 60 days ago | COMPLETED |
  Then the flight should be considered Eligible
 
Scenario: Old flight
  Given Todd has just joined the Frequent Flyer program
  And Todd asks for the following flight to be credited to his account:
    | Flight Number | Flight Date  | Status    |
    | FH-87         | 100 days ago | COMPLETED |
  Then the flight should be considered Ineligible
 
Scenario: Not a Flying High flight
  Given Todd has just joined the Frequent Flyer program
  And Todd asks for the following flight to be credited to his account:
    | Flight Number | Flight Date | Status    |
    | OH-101        | 60 days ago | COMPLETED |
  Then the flight should be considered Ineligible
 
Scenario: Flights must be completed
  Given Todd has just joined the Frequent Flyer program
  And Todd asks for the following flight to be credited to his account:
    | Flight Number | Flight Date | Status    |
    | FH-99         | 60 days ago | CANCELED |
  Then the flight should be considered Ineligible
 
Scenario: Flights must have taken place
  Given Todd has just joined the Frequent Flyer program
  And Todd asks for the following flight to be credited to his account:
    | Flight Number | Flight Date    | Status    |
    | FH-99         | In 5 days time | CONFIRMED |
  Then the flight should be considered Ineligible

这开始变得相当冗长了。用很多类似的场景来描述一组相关的业务规则是一种不好的做法;重复使得场景更难维护。此外,在前几个几乎相同的场景之后,读者很可能会略过后续场景并错过重要细节,这使得它们成为一种糟糕的沟通工具。

This is starting to get quite wordy. Having a lot of similar scenarios to describe a set of related business rules is a poor practice; the duplication makes the scenarios harder to maintain. In addition, after the first couple of almost-identical scenarios, readers are likely to just skim over the subsequent ones and miss important details, making them a poor communication tool.

在 BDD 中编写场景时,一个好的经验法则是“少即是多”。通常,您可以使用单个场景和总结不同情况的示例表更简洁、更有效地描述行为。要在 Cucumber 中执行此操作,我们需要使用Scenario Outline关键字而不是传统的Scenario。A Scenario Outline(或Scenario Template)使用传统的 Given ... When ... Then 步骤,但包括将在示例表中定义的变量的占位符。

When you’re writing scenarios in BDD, a good rule of thumb is “less is more.” You can often describe behavior more concisely and more effectively using a single scenario and a table of examples that summarizes the different cases. To do this in Cucumber we need to use the Scenario Outline keyword instead of a conventional Scenario. A Scenario Outline (or Scenario Template) uses the conventional Given ... When ... Then steps but includes placeholders for variables that will be defined in a table of examples.

一旦我们定义了模板结构,我们就使用Examples(或Scenarios)关键字列出说明我们想要描述的业务规则的示例。例如,您可以使用以下清单中的示例表更简洁地表达清单 7.1 中的场景。

Once we have defined the template structure, we use the Examples (or Scenarios) keyword to list the examples that illustrate the business rule we want to describe. For example, you could express the scenarios in listing 7.1 more succinctly using an example table like in the following listing.

清单 7.2 以场景大纲形式表达的场景

Listing 7.2 Scenarios expressed as a scenario outline

场景概要:可以申领过去 90 天内符合条件的航班      。❶
  鉴于Todd 刚刚加入飞行常客计划                     
  托德要求将以下航班记入他的账户:     
    | 航班号 | 航班日期 | 状态 |                             
    | <航班> | <日期> | <状态> |                             
  那么该航班应被视为<资格>
  例子
    | 航班 | 日期 | 状态 | 资格 | 原因 |     
    | FH-99 | -60 天 | 已完成 | 符合条件 | |     
    | FH-87 | -100 天| 已完成 | 不合格 | 太旧 |     
    | OH-101 | -60 天 | 已完成 | 不符合条件 | 不同的航空公司 |     
    | FH-99 | -60 天 | 已取消 | 不符合资格 | 必须完成 |     
    | FH-99 | +5 天 | 已确认 | 不符合条件 | 尚未发生 |     
Scenario Outline: Eligible flights in the past 90 days can be claimed.      
  Given Todd has just joined the Frequent Flyer program                     
  And Todd asks for the following flight to be credited to his account:.    
    | Flight Number   | Flight Date | Status   |                            
    | <Flight>        | < Date>     | <Status> |                            
  Then the flight should be considered <Eligibility>
  Examples:                                                                 
    | Flight | Date     | Status    | Eligibility | Reason             |    
    | FH-99  | -60 days | COMPLETED | Eligible    |                    |    
    | FH-87  | -100 days| COMPLETED | Ineligible  | Too old            |    
    | OH-101 | -60 days | COMPLETED | Ineligible  | Different airline  |    
    | FH-99  | -60 days | CANCELLED | Ineligible  | Must be completed  |    
    | FH-99  | +5 days  | CONFIRMED | Ineligible  | Hasn’t taken place |    

当我们使用表格时,我们需要使用场景大纲关键字。

When we use tables we need to use the Scenario Outline keyword.

用更通用的术语表达规则。示例表中的数据被传递到步骤中。

Express the rule in more generic terms. Data from the example table is passed into the steps.

Examples 关键字表示示例表的开始。

The Examples keyword indicates the start of a table of examples.

使用几个精心挑选的例子总结不同的情况。将针对表中的每一行数据检查该场景。

Summarize the different cases using a few well-chosen examples. The scenario will be checked for each row of data in the table.

这实际上是将五个场景合并为一个场景——该场景将运行五次,每次使用示例表中一行的值。表中的数据通过尖括号中的字段名称传递到每个步骤中:<Flight>、,<Date>等等。您已将四个冗长的场景简化为一个简洁的场景和一个示例表!

This is effectively five scenarios wrapped into one—the scenario will be run five times, each time with the values from one row in the example table. Data from the table is passed into each step via the field names in angle brackets: <Flight>, <Date>, and so forth. You’ve reduced four wordy scenarios into a single concise scenario and a table of examples!

当您使用这样的示例数据表时,将针对表中的每一行检查一次场景,这相当于五个单独的场景。但以表格形式呈现数据可以更容易地发现模式并更全面地了解问题。它还可以轻松描述和探索边界条件和边缘情况(这是您在前面的示例中所做的),并且它会产生出色的动态文档(见图 7.6)。

When you use a table of sample data like this, the scenario will be checked once for each row in the table, making this the equivalent of five separate scenarios. But presenting data in tabular form can make it easier to spot patterns and get a more holistic view of the problem. It also makes it easy to describe and explore boundary conditions and edge cases (which is what you did in the preceding examples), and it produces excellent living documentation (see figure 7.6).

图 7.6 表格数据也能生成出色的动态文档。

Figure 7.6 Tabular data also produces great living documentation.

7.3.3 待定场景

7.3.3 Pending scenarios

有时我们可能希望在验收测试套件中记录某个功能,然后再详细阐述它,作为未来工作的占位符。您只需记录功能和场景标题即可,而无需填写详细步骤。

Sometimes we might want to record a feature in your acceptance test suite before it has been fleshed out in detail, as a placeholder for future work. You can do this by simply recording the feature and scenario titles, without filling in the detailed steps.

特色:使用飞行常客积分预订航班
 
  作为常旅客计划经理
  我希望会员能够用积分为自己预订航班 
  以及他们的家人
  这样他们就会想赚更多的积分
 
  场景:使用积分预订个人航班
  场景:使用现金和积分预订个人航班
  场景:为多名乘客预订航班
Feature: Booking flights with Frequent Flyer points
 
  As a Frequent Flyer program manager
  I want members to be able to book flights with their points for themselves 
  and their families
  So that they will want to earn more points
 
  Scenario: Booking an individual flight using points
  Scenario: Booking an individual flight using money and points
  Scenario: Booking a flight for more than one passenger

此功能现在将出现在您的动态文档中,提醒您尚未完成的工作。这些场景将被报告为待处理,这意味着它们已被记录但尚未完成。实施的(见图 7.7)。

This feature will now figure in your living documentation, as a reminder of work that has yet to be done. The scenarios will be reported as pending, meaning they have been recorded but not yet implemented (see figure 7.7).

图 7.7 未实现的场景在报告中标记为待定。

Figure 7.7 Unimplemented scenarios are marked as pending in the reports.

7.4 使用特性文件和标签组织场景

7.4 Organizing your scenarios using feature files and tags

在实际项目中,场景可能会变得非常多,因此保持它们井然有序非常重要。您可能还需要能够以不同的方式识别和分组场景;例如,您可能希望区分与 UI 相关的场景和批处理场景,或者识别与跨功能关注相关的场景。在本节中,您将学习如何组织和分组场景,以使其更易于理解和维护。

In real-world projects, scenarios can become quite numerous, and it’s important to keep them well organized. You may also need to be able to identify and group scenarios in different ways; for example, you might want to distinguish UI-related scenarios from batch-processing scenarios or identify the scenarios related to cross-functional concerns. In this section, you’ll learn how to organize and group your scenarios to make them easier to understand and maintain.

7.4.1 场景放入功能文件中

7.4.1 The scenarios go in a feature file

场景的作用是说明某个功能,您可以将描述特定功能的所有场景放在一个文件中,通常使用概括该功能的名称(例如,earning_points_from_flights.feature)。我们将这些文件称为功能文件,它们通常使用 .feature 后缀。它们可以在简单的文本编辑器中读取和编辑,尽管大多数现代 IDE 也存在插件。在本章的其余部分中,我们将这些文件称为功能文件,无论使用什么文件后缀。

The role of a scenario is to illustrate a feature, and you place all the scenarios that describe a particular feature in a single file, usually with a name that summarizes the feature (e.g., earning_points_from_flights.feature). We call these files feature files, and they conventionally use the .feature suffix. They can be read and edited in a simple text editor, though plug-ins also exist for most modern IDEs. During the rest of the chapter, we’ll refer to these files as feature files, regardless of the file suffix used.

正如您在第 2 章中看到的,这些场景是项目源代码的一部分,它们将受到版本控制。许多团队在“三个朋友”需求发现会议后不久编写功能文件,并在会议结束时将它们存储在源代码存储库中。

As you saw in chapter 2, these scenarios are part of the project’s source code, and they’ll be placed under version control. Many teams write the feature files shortly after the “Three Amigos” requirements discovery sessions and store them in the source code repository at the end of the meetings.

用于存储功能文件的确切文件结构因项目而异;Java 项目中的常见惯例是使用 src/test/resources 文件夹下的 features 目录(见图 7.8)。

The exact file structure used to store the feature files varies from project to project; a common convention in Java projects is to use a features directory under the src/test/resources folder (see figure 7.8).

图 7.8 功能文件作为项目源代码的一部分存储在纯文本文件中(此示例来自 Java 中的 Cucumber 项目)。

Figure 7.8 Feature files are stored in plain-text files as part of the project source code (this example is from a Cucumber project in Java).

有些团队为每个用户故事设置了单独的功能文件。这通常不是一个好主意。4请记住,故事临时的规划工件,可以在迭代结束时忽略,但功能是利益相关者可以理解和关联的宝贵功能单元。按功能而不是故事来组织场景可以更轻松地生成有意义的动态文档。在迭代期间,将场景与相应的故事相关联以进行规划和报告会很有用,但主要关联应该是场景与其所关联的功能之间的关联。说明。

Some teams have separate feature files for each User Story. This is generally a bad idea.4 Remember, stories are transitory planning artifacts that can be disregarded at the end of an iteration, but features are valuable units of functionality that stakeholders can understand and relate to. Organizing scenarios in terms of features rather than stories makes it easier to generate meaningful living documentation. It can be useful to associate scenarios with the corresponding stories for planning and reporting purposes during an iteration, but the primary association should be between a scenario and the feature it illustrates.

7.4.2 一个特征文件可以包含一个或多个场景

7.4.2 A feature file can contain one or more scenarios

作为在第 5 章中讨论过,您可以使用具体示例来说明每个功能。您决定自动化的任何示例都将由相应功能文件中的单个场景表示。因此,功能文件将包含说明其预期行为的所有示例。这些示例不仅说明了简单情况,还说明了替代路径和边缘情况,这些情况充实了功能在不同情况下的行为方式情况。

As discussed in chapter 5, you use concrete examples to illustrate each feature. Any example you decide to automate will be represented by a single scenario in the corresponding feature file. As a result, the feature file will contain all the examples that illustrate its expected behavior. These examples illustrate not only the simple cases but also alternative paths and edge cases that flesh out how the feature behaves in different situations.

7.4.3 组织功能文件

7.4.3 Organizing the feature files

什么时候当你开始在项目中获得大量功能文件时,重要的是要以一种易于查找和浏览的方式组织它们。一个好方法是将它们放入子目录中,如图 7.8 所示。当然,你需要决定哪种目录结构最适合你的团队。有几种方法可以做到这一点这。

When you start to get a large number of feature files in your project, it’s important to keep them organized in a way that makes them easy to find and browse. A good way to do this is to place them into subdirectories, as illustrated in figure 7.8. Of course, you’ll need to decide what directory structure makes the most sense for your team. There are a few ways you can do this.

7.4.4 使用扁平目录结构

7.4.4 Using a flat directory structure

完全可以将所有功能文件保存在同一目录中(例如,对于 Java 项目,保存在 src/test/resources/features 下)。但是,对于只有少量功能文件的小型项目,这很快就会变得令人困惑的。

It is perfectly possible to keep all your feature files in the same directory (e.g., under src/test/resources/features for a Java project). However, for anything other than a small project with only a few feature files, this can quickly become confusing.

7.4.5 按故事或产品增量组织功能文件

7.4.5 Organizing feature files by stories or product increments

一些团队喜欢为每个用户故事设置一个单独的功能文件。虽然从表面上看,这似乎是一种合乎逻辑的方法,但在实践中并不理想。用户故事是一种规划工件,旨在帮助组织团队的工作,以便将功能交到用户手中。另一方面,功能是一种独特的功能,可能需要几个用户故事才能完成。有时,新的用户故事可能会改变或替换为先前的用户故事定义的场景,这使得很难知道特定场景应该放在哪里。

Some teams like to have a separate feature file for each User Story. While this seems like a logical approach on the surface, in practice it is not ideal. A User Story is a planning artifact, designed to help organize the work of the team to get a feature into the hands of the users. A feature, on the other hand, is a distinct piece of functionality, that might take several User Stories to complete. And sometimes a new User Story may change or replace the scenarios defined for a previous User Story, which makes it hard to know where a particular scenario should go.

其他团队根据产品发布来分组功能;他们为每个迭代或发布设置一个目录,所有与该发布相关的功能都存储在该目录中。但是,这种方法也不理想。通常,功能的开发可以分为几个迭代,这要么很难知道功能文件应该放在哪里,要么引入不必要的重复,因为多个功能文件包含实际上与同一功能相关的场景。

Other teams group their features around product releases; they have a directory for each iteration or release, and all the features related to that release are stored in that directory. However, this approach isn’t ideal either. Very often, the development of a feature can be split across several iterations, which either makes it hard to know where the feature file should live or introduces unnecessary duplication with multiple feature files containing scenarios that are actually related to the same feature.

但这两种方法还存在一个更根本的问题。它们将项目交付产品文档混为一谈。功能文件及其包含的场景旨在成为产品文档;它们描述了应用程序的功能、它应用的业务规则以及它支持的用户行为。此功能将随着每次新的迭代或发布而发展和增长,但功能和发布之间并不总是存在明确的关系,尤其是在应用程序发展的过程中。换句话说,您不会将用户手册或需求规范文档按每个发布一章来组织,因此您可能不应该以这种方式组织功能文件任何一个。

But there is also a more fundamental problem with both of these approaches. They confuse project delivery with product documentation. Feature files, and the scenarios they contain, are intended to be product documentation; they describe what the application does, what business rules it applies, and what user behavior it supports. This functionality will evolve and grow with each new iteration or release, but there is not always a clean relationship between a feature and a release, particularly as the application evolves. In other words, you wouldn’t organize a user manual or a requirement specification document with one chapter per release, so you probably shouldn’t organize your feature files this way either.

7.4.6 按功能和能力组织功能文件

7.4.6 Organizing feature files by functionality and capability

一般来说总的来说,组织功能文件的更好方法是根据业务功能。功能文件是动态文档;它们帮助我们了解应用程序的功能,因此以描述应用程序的方式组织功能文件是有意义的。

Generally speaking, a better way to organize your feature files is in terms of business capabilities. Feature files are living documentation; they help us understand what the application does, so it makes sense to organize your feature files in a way that describes the app.

如果您已经按照第 3 章和第 4 章中描述的功能对功能进行了组织,那么为每项功能创建一个目录是非常自然的。对于较大的项目,您可能首先按照更高级别的功能对功能进行组织,然后按照更细粒度的功能对这些功能区域内的功能进行分组。您可以在此处看到一个示例:

If you’ve organized your features in terms of capabilities, as described in chapters 3 and 4, it’s very natural to create a directory for each capability. And for larger projects, you might first organize your features in terms of higher-level functionalities, and then group features within these functional areas in terms of more fine-grained capabilities. You can see an example here:

+ 功能 
    + 常旅客
        + 账户
            + 注册功能 
            + 更改密码功能 
            + 查看航班历史记录功能 
        + 预订航班
            + book_with_points.feature 
            + 使用积分和现金预订功能 
        + 赚取积分
            + 通过航班赚取积分功能
            + 通过酒店住宿赚取积分功能
            + 通过租车赚取积分功能
+ features 
    + frequent_flyers
        + account
            + register.feature 
            + change_password.feature 
            + view_flight_history.feature 
        + book_a_flight
            + book_with_points.feature 
            + book_with_points_and_cash.feature 
        + earn_points
            + earn_points_from_flights.feature
            + earn_points_from_hotel_stays.feature
            + earn_points_from_car_rentals.feature

正如您在本例中看到的,顶级目录概述了应用程序的功能,有点像需求文档中的部分。如果您想了解有关特定功能领域的更多信息,可以深入到相应的目录中,查看支持哪些特定功能。正如您稍后将看到的,Serenity BDD 报告工具 ( www.serenity-bdd.info ) 可以配置为使用此目录结构按特性、功能、标签等对可执行需求进行分组向前。

As you can see in this example, the top-level directories give an overview of what the application does, a bit like sections in a requirements document. And if you want to know more about a particular functional area, you can drill down into the corresponding directory to see what specific capabilities are supported. As you’ll see later, the Serenity BDD reporting tool (www.serenity-bdd.info) can be configured to use this directory structure to group executable requirements by features, capabilities, tags, and so forth.

7.4.7 使用标签注释你的场景

7.4.7 Annotating your scenarios with tags

你已经看到了如何将功能文件组织到子目录中,以反映需求的结构。但通常最好有其他方法来对场景进行分类,例如按技术组件或模块,或者按可能适用于许多功能的某些跨切业务规则。幸运的是,这很容易做到。Gherkin 允许您向功能、场景甚至示例表添加标签。标签可用于过滤报告和测试执行,并在您查看功能或场景时提供额外的上下文。

You’ve seen how you can organize your feature files into subdirectories that mirror your requirements’ structure. But often it’s nice to have other ways to categorize your scenarios, such as by technical component or module, or by some cross-cutting business rule that may apply to many features. Fortunately, this is quite easy to do. Gherkin lets you add tags to features, scenarios, and even example tables. Tags can be used to filter reports and test execution, and to provide extra context when you look at a feature or scenario.

例如,我们可以使用如下标签来表明某个功能与飞行常客模块相关:

For example, we could indicate that a feature is related to the Frequent Flyer module with a tag like this:

@frequentflyer 
特色:通过航班赚取飞行常客积分
 
  场景:旅行者根据旅行距离赚取积分
    ...
@frequentflyer 
Feature: Earning Frequent Flyer points from flights
 
  Scenario: Travelers earn points based on the distance traveled
    ...

我们可以在场景级别添加标签,以强调第一个场景特别重要:

We could highlight that the first scenario is particularly important by adding a tag at the scenario level:

@frequentflyer 
特色:通过航班赚取飞行常客积分
 
  @重要的 
  场景:旅行者根据旅行距离赚取积分
    ...
@frequentflyer 
Feature: Earning Frequent Flyer points from flights
 
  @important 
  Scenario: Travellers earn points based on the distance traveled
    ...

标签也是根据其他跨功能关注点对场景进行分类、识别系统相关部分以及帮助组织测试执行的好方法。例如,您可能希望标记所有 Web 测试,或将某些测试标记为运行缓慢,以便在自动构建过程中将它们分组在一起。在以下示例中,该场景被标记为 UI 测试(并且很重要):

Tags are also a great way to categorize scenarios by other cross-functional concerns, to identify related parts of the system, and to help organize test execution. For example, you might want to flag all of the web tests, or mark certain tests as being slow, so that they can be grouped together during the automated build process. In the following example, the scenario is flagged as a UI test (as well as being important):

@frequentflyer 
特色:通过航班赚取飞行常客积分
 
  @重要 @ui 
  场景:旅行者根据旅行距离赚取积分
    ...
@frequentflyer 
Feature: Earning Frequent Flyer points from flights
 
  @important @ui 
  Scenario: Travelers earn points based on the distance traveled
    ...

这样,在执行测试时,您可以配置一个过滤器以仅运行带有特定标签的测试(或不带有特定标签的测试;您将在第 8 章中看到如何执行此操作)。

This way, when executing the tests, you can configure a filter to only run the tests with a particular tag (or without a particular tag; you’ll see how to do this in chapter 8).

标签可以由任何非空白字符组成,一些报告工具利用这一事实来提供与其他工具的更高级集成。例如,许多项目将有关功能或用户故事的详细信息存储在问题跟踪软件(如 Atlassian 的 JIRA)中。许多团队使用标签将功能或单个场景与相应的问题关联起来,这既是为了获取信息,也是为了报告工具可以使用这些数据创建指向相应问题的链接。例如,第二种情况如下所示:

Tags can be made up of any nonblank characters, and this fact is used by some reporting tools to provide more advanced integration with other tools. For example, many projects store details about features or User Stories in issue-tracking software such as Atlassian’s JIRA. Many teams use tags to relate a feature or an individual scenario back to the corresponding issue, both for information and so that reporting tools can use this data to create a link back to the corresponding issue. For example, the second scenario is as illustrated here:

@frequentflyer 
特色:通过航班赚取飞行常客积分
 
  @重要的 
  场景:旅行者根据旅行距离赚取积分
    ...
 
  @问题:FF-123
  场景:商务舱旅客额外积分
    ...
@frequentflyer 
Feature: Earning Frequent Flyer points from flights
 
  @important 
  Scenario: Travelers earn points based on the distance traveled
    ...
 
  @issue:FF-123
  Scenario: Travellers extra points in Business class
    ...

Serenity BDD 和 Serenity/JS 特别使用了简单的标记约定,以便更轻松地通过标记对场景进行分组:

Serenity BDD and Serenity/JS in particular use a simple tagging convention to make it easier to group scenarios by tags:

@<标签类型>:<标签名称>
@<tag-type>:<tag-name>

例如,假设项目利益相关者希望了解系统中各个组件的行为方式以及测试效果。以下功能定义了与系统中两个不同组件相关的两个场景:身份验证组件以及账户组件

For example, imagine that project stakeholders want to see how various components in a system behave and how well they are tested. The following feature defines two scenarios that related to two different components in the system: the authentication component and the accounts component:

特色:注册常旅客计划
 
  @component:身份验证
  场景:用户可以在线注册
 
  @component:帐户
  场景:用户可以申请过去三天内完成的航班积分 
            个月
Feature: Signing up to the Frequent Flyer program
 
  @component:authentication
  Scenario: Users can sign up online
 
  @component:accounts
  Scenario: Users can request credits for flights completed in the past three 
            months

使用此约定,您可以配置 Serenity BDD 以包含按这些组件分组的测试结果。这为每个组件的测试覆盖率提供了简洁的概述(见图 7.9)。更重要的是,从 BDD 的角度来看,此概述还为您提供了有关特定组件的文档。

Using this convention, you can configure Serenity BDD to include test results grouped by these components. This gives a succinct overview of test coverage for each component (see figure 7.9). More importantly, from a BDD perspective, this overview also gives you a place to go for documentation about a particular component.

图 7.9 使用标签组织测试报告

Figure 7.9 Using tags to organize the test reports

BDD 工具还允许您编写钩子— 在执行带有特定标签的场景之前或之后执行的方法。这是为某些类型的测试设置测试环境或事后清理的好方法。您将在第 8 章中了解有关如何执行此操作的更多信息。

BDD tools also let you write hooks—methods that will be executed before or after a scenario with a specific tag is executed. This is a great way to set up a test environment for certain types of tests, or to clean up afterward. You’ll learn more about how to do this in chapter 8.

您还可以使用标签作为强大的报告工具(见图 7.9)。例如,Serenity BDD 会报告标签,允许您按标签或标签类型过滤测试结果,这为基于文件夹的需求层次结构提供了一种替代方案,并有助于将注意力集中在真正重要的功能或组件上。商人。

You can also use tags as a powerful reporting tool (see figure 7.9). For example, Serenity BDD reports on tags, allowing you to filter test results by tag or tag type, which provides an alternative to the folder-based requirements hierarchy and can help to focus attention on functionality or components that really matter to businesspeople.

7.4.8 提供背景和上下文以避免重复

7.4.8 Provide background and context to avoid duplication

其他场景中出现重复的常见情况是多个场景以相同的步骤开始。例如,飞行常客会员可以登录飞行常客网站查看积分和状态、预订航班等。以下 Gherkin 场景与登录此网站有关:

Another common way that duplication slips into scenarios is when several scenarios start off with the same steps. For example, Frequent Flyer members can log on to the Frequent Flyer website to consult their points and status, book flights, and so forth. The following Gherkin scenarios relate to logging on to this site:

特色:登录‘我的飞翔’网站
  常旅客会员可以在“My Flying High”网站上注册
  使用他们的飞行常客号码和他们提供的密码
  
  场景:登录成功
    鉴于马丁是常旅客会员                         
    马丁已经在网上注册了,密码是“secret”     
    Martin 使用密码“secret”登录
时    那么他应该被授予访问网站的权限
 
  场景:使用错误密码登录
    鉴于马丁是常旅客会员                         
    马丁已经在网上注册了,密码是“secret”     
    Martin 使用错误密码登录
时    然后他应该被告知他的密码不正确
  
  场景:使用过期的帐户登录
    鉴于马丁是常旅客会员                         
    马丁已经在网上注册了,密码是“secret”     
    账户已过期
    Martin 使用密码“secret”登录
时    然后他应该被告知他的帐户已过期
    应邀请他更新他的账户
Feature: Logging on to the 'My Flying High' website
  Frequent Flyer members can register on the 'My Flying High' website
  using their Frequent Flyer number and a password that they provide
  
  Scenario: Logging on successfully
    Given Martin is a Frequent Flyer member                         
    And Martin has registered online with a password of 'secret'    
    When Martin logs on with password 'secret'
    Then he should be given access to the site
 
  Scenario: Logging on with an incorrect password
    Given Martin is a Frequent Flyer member                         
    And Martin has registered online with a password of 'secret'    
    When Martin logs on with password 'wrong'
    Then he should be informed that his password was incorrect
  
  Scenario: Logging on with an expired account
    Given Martin is a Frequent Flyer member                         
    And Martin has registered online with a password of 'secret'    
    But the account has expired
    When Martin logs on with password 'secret'
    Then he should be informed that his account has expired
    And he should be invited to renew his account

注意重复的步骤。

Note the duplicated steps.

这些场景包含大量重复内容,这使得它们更难阅读和维护。此外,如果您需要更改其中一个重复的步骤,则需要在多个地方进行更改。

These scenarios contain a lot of repetition, which makes them harder to read and to maintain. In addition, if you ever need to change one of the duplicated steps, you’ll need to do so in several places.

Background您可以使用关键字避免重复前两个步骤,如下所示:

You can avoid having to repeat the first two steps by using the Background keyword, as shown here:

特色:登录‘我的飞翔’网站
  常旅客会员可以在“My Flying High”网站上注册
  使用他们的飞行常客号码和他们提供的密码
 
  背景:Martin 已在该网站注册                     
    鉴于马丁是常旅客会员                        
    马丁已经在网上注册了,密码是“secret”    
  
  场景:登录成功                                
    Martin 使用密码 'secret' 登录时                     
    那么他应该被授予访问该网站的权限                     
  
  场景:使用错误密码登录
    Martin 使用错误密码登录
时    然后他应该被告知他的密码不正确
  
  场景:使用过期的帐户登录
    鉴于帐户已过期
    Martin 使用密码“secret”登录
时    然后他应该被告知他的帐户已过期
    应邀请他更新他的账户
Feature: Logging on to the 'My Flying High' website
  Frequent Flyer members can register on the 'My Flying High' website
  using their Frequent Flyer number and a password that they provide
 
  Background: Martin is registered on the site                     
    Given Martin is a Frequent Flyer member                        
    And Martin has registered online with a password of 'secret'   
  
  Scenario: Logging on successfully                                
    When Martin logs on with password 'secret'                     
    Then he should be given access to the site                     
  
  Scenario: Logging on with an incorrect password
    When Martin logs on with password 'wrong'
    Then he should be informed that his password was incorrect
  
  Scenario: Logging on with an expired account
    Given the account has expired
    When Martin logs on with password 'secret'
    Then he should be informed that his account has expired
    And he should be invited to renew his account

这些步骤将在每个场景之前运行。

These steps will be run before each scenario.

场景更加集中。

The scenarios are more focused.

关键词Background可让您指定功能中每个场景之前要运行的步骤。您可以使用此功能避免在每个场景中重复执行步骤,这也有助于将注意力集中在重要部分上每个设想。

The Background keyword lets you specify steps that will be run before each scenario in the feature. You can use this to avoid duplicating steps in each scenario, which also helps focus attention on the important bits of each scenario.

7.5 规则与示例

7.5 Rules and examples

黄瓜 6引入一些新的关键字,可让您更清晰地定义功能的业务规则,并为每个业务规则分组示例和反例(参见清单 7.3)。Rule关键字表示功能上下文中的一条业务规则。下面是Rule一个或多个用于说明此规则的场景。

Cucumber 6 introduced some new keywords that let you define the business rules for a feature more clearly and group examples and counterexamples for each business rule (see listing 7.3). The Rule keyword represents a single business rule, in the context of a feature. Underneath the Rule you group one or more scenarios that illustrate this rule.

关键字Example用于表示某个事物的具体例子或反例Rule。关键字实际上是关键字Example的同义词Scenario我们已经看到到目前为止;你可以使用它们可以互换。

The Example keyword is used to indicate a concrete example or counterexample of a Rule. The Example keyword is actually a synonym of the Scenario keyword we have seen up until now; you can use them interchangeably.

清单 7.3 使用 Cucumber 6 规则和示例关键字

Listing 7.3 Using the Cucumber 6 Rule and Example keywords

功能:会员之间转移积分
 
  作为常旅客会员
  我想将不需要的积分转移给我的家人
  为了不浪费积分
 
  背景    鉴于以下常旅客会员
      | 名字 | 姓氏 | 家庭代码 |
      | 莎拉 | 麻雀 | FAM-101 |
      | 史蒂夫 | 斯帕罗 | FAM-101 |
      | 弗雷德 | 猎鹰 | FAM-202 |
 
  规则:同一家庭的常旅客会员可以转移积分
    例如:现有会员之间转移积分
      假设飞行常客账户余额如下:
        | 所有者 | 积分 | 状态积分 |
        | 莎拉 | 100000 | 800 |
        | 史蒂夫 | 50000 | 50 |
      Sarah 向 Steve 转 40,000 点积分
时      那么账目应该如下:
        | 所有者 | 积分 | 状态积分 |
        | 莎拉 | 60000 | 800 |
        | 史蒂夫 | 90000 | 50 |
    例如:非家庭成员之间转移积分
      假设常旅客帐户余额如下:
        | 所有者 | 积分 | 状态积分 |
        | 莎拉 | 100000 | 800 |
        | 弗雷德 | 20000 | 50 |
      Sarah 尝试将 10,000 点积分转给 Fred 时
      那么不应允许转让
 
  规则:会员不得转移超过其现有积分的积分
    例如:史蒂夫试图转移比他拥有的更多的积分
      假设飞行常客账户余额如下:
        | 所有者 | 积分 | 状态积分 |
        | 莎拉 | 100000 | 800 |
        | 史蒂夫 | 50000 | 50 |
      史蒂夫试图将 100000 积分转给莎拉时
      那么不应允许转让
Feature: Transferring points between members
 
  As a Frequent Flyer Member
  I want to transfer points that I don't need to members of my family
  So that the points don't go to waste
 
  Background:
    Given the following Frequent Flyer members
      | Name  | Surname  | Family Code |
      | Sarah | Sparrow  | FAM-101     |
      | Steve | Sparrow  | FAM-101     |
      | Fred  | Falcon   | FAM-202     |
 
  Rule: Frequent Flyer members in the same family can transfer points
    Example: Transfer points between existing members
      Given the following Frequent Flyer account balances:
        | owner | points | status-points |
        | Sarah | 100000 | 800           |
        | Steve | 50000  | 50            |
      When Sarah transfers 40000 points to Steve
      Then the accounts should be as follows:
        | owner | points | status-points |
        | Sarah | 60000  | 800           |
        | Steve | 90000  | 50            |
    Example: Transfer points between non-family members
      Given the following Frequent FLyer account balances:
        | owner | points | status-points |
        | Sarah | 100000 | 800           |
        | Fred  | 20000  | 50            |
      When Sarah tries to transfer 10000 points to Fred
      Then the transfer should not be allowed
 
  Rule: Members cannot transfer more points than they have
    Example: Steve tries to transfer more points than he has
      Given the following Frequent Flyer account balances:
        | owner | points | status-points |
        | Sarah | 100000 | 800           |
        | Steve | 50000  | 50            |
      When Steve tries to transfer 100000 points to Sarah
      Then the transfer should not be allowed

7.6 表达场景:模式和反模式

7.6 Expressive scenarios: Patterns and anti-patterns

现在你已经看到了机制在 Gherkin 中编写场景,是时候将事情提升到一个新的水平了。在本节中,我们将不仅了解场景的结构和编写方式,还将了解一个好的场景需要哪些要素。

Now that you’ve seen the mechanics of writing scenarios in Gherkin, it’s time to take things to the next level. In this section we’ll go beyond just seeing how scenarios are structured and written and look at what goes into a good scenario.

7.6.1 制作美味小黄瓜的艺术

7.6.1 The art of good Gherkin

正如我们所见,场景旨在通过自由文本描述和可执行示例的混合来记录和说明一项功能,所有这些都是用业务人员可以理解的语言编写的。它们既可以作为业务需求的描述,也可以作为支持此需求的功能的文档(见图 7.10)。让我们来分析一下。

As we have seen, scenarios are intended to document and illustrate a feature with a mixture of free-text description and executable examples, all written in a language businesspeople can understand. They act both as a description of a business need, and as documentation of the feature that supports this need (see figure 7.10). Let’s unpack this.

Gherkin 场景首先是一种讨论和记录业务需求的方法。但它们是以可执行格式编写的,这意味着它们可以很容易地转换为自动化测试并作为正常构建过程的一部分运行。这可以缩短反馈周期,并为测试优先的开发实践(例如 ATDD 和 TDD)开辟道路。

Gherkin scenarios are first and foremost a way to discuss and record business requirements. But they are written in an executable format, which means they can be readily turned into automated tests and run as part of your normal build process. This leads to faster feedback cycles and opens the way to test-first development practices, such as ATDD and TDD.

图 7.10 Gherkin 场景同时是需求规范、可执行测试和动态文档。

Figure 7.10 Gherkin scenarios are simultaneously requirements specifications, executable tests, and living documentation.

传统需求文档的一个问题是它们很快就会过时。但是,当它们以可执行格式编写并可针对正在运行的应用程序进行测试时,它们就可以充当活文档我们知道它们不仅反映了软件应该做什么,还反映了软件做了什么。当它们在每次构建时执行时,我们可以确信它们确实反映了应用程序的当前行为。

One of the problems with traditional requirements documents is that they quickly become out of date. But when they are written in an executable format, and can be tested against the running application, they act as living documentation. We know they reflect not only what the software should do, but also what it does. And when they are executed with every build, we can be confident that they do indeed reflect the current behavior of the application.

但编写好的 Gherkin 场景并非易事。需要练习和协作才能做好。就像好的散文一样,编写清晰易读的场景很难。通常,Gherkin 场景越容易理解,作者为使其更容易理解付出的努力就越多。

But writing good Gherkin scenarios is not an easy task. It takes practice and collaboration to get it right. Like good prose, writing scenarios that are clean and easy to read is hard. Very often, the easier a Gherkin scenario is to understand, the more effort the authors have put in to make it so.

不幸的是,编写糟糕的 Gherkin 场景在现实生活中随处可见。许多现有的 Cucumber 测试套件都是由没有接触过 BDD 实践的人编写的(现在仍然是),或者认为 Cucumber 是传统的测试脚本工具,而不是协作和文档工具。

And poorly written Gherkin scenarios are, unfortunately, very easy to find in the wild. Many existing Cucumber test suites were (and still are) written by people who have not been exposed to BDD practices, or who have assumed that Cucumber is a traditional test-scripting tool and not the collaboration and documentation tool that it is.

写得不好的 Gherkin 不仅看起来难看,还会对您的项目造成不利影响!与以动态文档和可执行规范为精神编写的场景相比,不可靠的 Gherkin 场景更脆弱、更难理解,维护成本也更高。如果它们失败了,很难知道哪里出了问题,这会降低对整个测试套件的信心。

Poorly written Gherkin is not just hard on the eyes. It is bad for your project too! Dodgy Gherkin scenarios are more fragile, harder to understand, and more expensive to maintain than scenarios written in the spirit of living documentation and executable specifications. If they fail, it is harder to know what went wrong, which reduces confidence in the test suite as a whole.

它们也往往更难扩展和更新。我曾与一个团队合作,他们报告称,当他们使用更具声明性的方法重构旧的 Cucumber 测试脚本时,编写和自动化新场景所需的时间减少了 80%。在本节的其余部分,我们将介绍一些需要避免的陷阱,您将学到一些技巧,以确保您的场景保持干净、可读且易于维护。

They also tend to be much harder to extend and update. One team I worked with reported gains of 80% in the time taken to write and automate new scenarios when they refactored their old Cucumber test scripts using a more declarative approach. In the rest of this section, we will look at some of the traps to avoid, and you will learn some tips on how to make sure your scenarios stay clean, readable, and easy to maintain.

7.6.2 坏小黄瓜长什么样

7.6.2 What bad Gherkin looks like

如果好的 Gherkin 就像海明威写的东西,坏的 Gherkin 就像你在去海滩的路上捡到的那些垃圾小说,或者洗衣机附带的翻译很差的说明书。例如,考虑以下列表中的场景。

If good Gherkin is like something written by Hemingway, bad Gherkin is like one of those trashy novels you pick up on the way to the beach, or the poorly translated instruction manual that came with your washing machine. Consider for example the scenario in the following listing.

清单 7.4 一个写得很差的 Gherkin 场景

Listing 7.4 A poorly written Gherkin scenario

场景:端到端酒店预订测试
  假设我登录了旅行预订应用程序
  然后我就在主页上
  然后我点击“新行程”标签
  然后我点击“酒店”下拉菜单项
  然后我就进入了酒店搜索页面
  我在城市字段中输入“巴黎”时
  将价格范围设置为“任意”
  将入住日期设置为“04-04-2019”
  输入的住宿天数为“2”
  将距离设置为“10”
  我点击“搜索”按钮
时  那么列出的酒店是:
    | 酒店名称 | 距离 | 空房情况 | 价格 | 游泳池 | 健身房 |
    |丽兹 | 3.2 |是的 | 400 |是 |是 |
    |萨沃伊 | 6.9 | 6.9是的 | 500 | 500尼 |是 |
  我从列表中选择“Ritz”时
  选择“使用积分支付”
  检查我是否有足够的积分
  然后我点击“支付”
  然后验证出现的消息
  确认价格正确
  然后我点击注销
Scenario: End-to-end hotel booking test
  Given I login to the Travel Booking App
  Then I am on the Home Page
  And I click the New Trip tab
  And I click on the Hotels dropdown menu item
  Then I am on the Hotel Search page
  When I Enter "Paris" into the city field
  And I set price range to "Any"
  And I set check-in date to "04-04-2019"
  And I enter number of nights to "2"
  And I set distance to "10"
  When I hit "search" button
  Then the hotels listed are:
    | Hotel Name | Distance | Availability | Price | Pool | Gym |
    | Ritz       | 3.2      | Yes          | 400   | Y    | Y   |
    | Savoy      | 6.9      | Yes          | 500   | N    | Y   |
  When I select "Ritz" from the list
  And I select "Pay with points"
  And check that I have enough points
  And I click "Pay"
  Then verify the message that appears
  And verify that the price is correct
  And I click on logout

这里有很多问题。这个场景很长而且很密集,是一系列单调的低级 UI 交互,很难看出它真正想要测试的是什么。如果按钮或字段发生变化,即使业务逻辑仍然有效,场景也会中断。此外,没有明确的目标或结果;这个场景似乎在执行许多不同的业务规则。如果这个场景失败了,很难确切知道到底是什么出了问题。

There are many things wrong here. The scenario is long and dense, a monotonous sequence of low-level UI interactions that make it hard to see what it is really trying to test. If the buttons or fields change, the scenario would break even if the business logic is still valid. Furthermore, there is no clear goal or outcome; the scenario seems to be exercising many different business rules. If this scenario failed, it would be quite hard to know exactly what was broken.

通常情况下,批评很容易,但表达如何做得更好却很难。在本节的其余部分,我们将介绍编写良好的 Gherkin 场景的一些关键特征,希望这些特征可以帮助您改进自己的场景出色地。

As often is the case, it is easy to criticize but more difficult to articulate how to do things better. In the rest of this section, we will go through some key characteristics that well-written Gherkin scenarios tend to share, and that can hopefully help you improve your own scenarios as well.

7.6.3 好的场景是声明性的,而不是命令性的

7.6.3 Good scenarios are declarative, not imperative

语法中,命令式是你在告诉别人做什么时使用的。例如,“把脏衣服从地板上拿起来放进洗衣篮里;然后收拾那些玩具。”另一方面,陈述式则侧重于你想要实现的结果,或者你希望发生的事情,而不是给出做什么的指示:“你的房间需要在晚餐时间前整理好。”

In grammar, the imperative mode is what you use when you are telling someone what to do. “Take your dirty clothes off the floor and put them in the laundry basket; then clean up those toys,” for example. A declarative style, on the other hand, focuses on the outcome you are trying to achieve, or what you would like to happen, rather than giving instructions on what to do: “Your room needs to be tidy by dinner time.”

或者,想象一下你和你的伴侣来到一家餐厅。如果你心情很急躁,你可以对迎接你的服务员说:“我想穿过房间到右边靠窗的第二张桌子坐下,请你给我们拿两份菜单。”或者,用更直接的语气说:“请给我一张靠窗的两人桌。”

Or, imagine you arrived at a restaurant with your partner. If you were in an imperative mood, you might say to the server who greets you, “I would like to walk across the room to the second table from the right just by the window and sit down, and for you to bring us two menus.” Alternatively, in a more declarative style, you could say, “A table for two by the window, please.”

在编程语言中,命令式风格关注的是我们如何执行任务。例如,如果我们想从年龄列表中找出平均年龄,命令式风格会将年龄相加,然后除以列表中的年龄数量:5

In programming languages, an imperative style focuses on how we perform a task. For example, if we wanted to find the average age from a list of ages, an imperative style would add up the ages and divide them by the number of ages in the list:5

val 年龄 = listOf(20,40,50,15,80);
总年龄 = 0
对于(年龄){
    总年龄 = 总年龄 + 年龄
}
val 平均年龄 = 总年龄 / 年龄.大小
val ages = listOf(20,40,50,15,80);
var totalAges = 0
for (age in ages) {
    totalAges = totalAges + age
}
val averageAge = totalAges / ages.size

在声明式编程风格(通常与函数式编程语言相关)中,您更关注想要实现的目标,而不是如何实现它。要找到平均年龄,您只需声明您想要年龄列表的平均值:

In a declarative style of programming (which is often associated with functional programming languages), you focus more on what you want to achieve than how you will achieve it. To find the average age, you would simply declare that you want the average of the list of ages:

val 年龄 = listOf(20,40,50,15,80);
val 平均年龄 = 年龄.平均();
val ages = listOf(20,40,50,15,80);
val averageAge = ages.average();

在 Gherkin 中,当我们说一个场景是用命令式风格编写的,我们的意思是它读起来就像一串低级指令。命令式场景不是描述业务规则和预期结果,而是专注于用户如何与应用程序进行非常精细的交互。它们不是描述用户想要做什么,而是专注于如何做。它们讨论要点击哪些按钮、要填写哪些字段或要进行哪些系统调用。

In Gherkin, when we say that a scenario is written in an imperative style, we mean that it reads like a list of low-level instructions. Rather than describing business rules and expected outcomes, imperative scenarios focus on how the user interacts with the application at a very granular level. Rather than describing what the user wants to do, they focus on the how. They talk about what buttons to click, what fields to fill in, or what system calls to make.

清单 7.4 中的场景是命令式风格的典型示例。以下步骤只是有关如何与应用程序交互的详细说明的长列表:

The scenario in listing 7.4 is a typical example of an imperative style. The following steps are just a long list of detailed instructions on how to interact with the application:

我在城市字段中输入“巴黎”时
将价格范围设置为“任意”
将入住日期设置为“04-04-2019”
输入的住宿天数为“2”
将距离设置为“10”
我点击“搜索”按钮时
When I Enter "Paris" into the city field
And I set price range to "Any"
And I set check-in date to "04-04-2019"
And I enter number of nights to "2"
And I set distance to "10"
When I hit "search" button

为什么这么糟糕?由于需要进行这么多点击和选择,因此很难看清这些步骤的整体作用。对细节的关注使得很难对整体流程或业务逻辑提供反馈。此外,这还使得场景更加难以操作,维护起来也更加困难。Gherkin 场景与实现它的屏幕紧密相关:如果任何用户界面细节发生变化,即使底层业务逻辑没有变化,场景也需要更新。

Why is this so bad? With all these clicks and selects, it’s hard to see what the steps are doing as a whole. The focus on detail makes it harder to give feedback on the overall flow or business logic. In addition, this makes the scenario more unwieldy and more work to maintain. The Gherkin scenario is tightly coupled to the screens that implement it: if any of the user interface details change, the scenario will need updating, even if the underlying business logic has not changed.

提示:好的场景建模的是业务行为,而不是系统交互。它们描述的是用户试图完成的任务,而不是用户执行这些任务需要进行的点击和选择。如果您遵循我们在前几章中学到的专注于描述业务行为和目标的技术,编写高质量的场景就会变得更加自然。

TIP Good scenarios model business behavior, not system interactions. They describe the tasks a user is trying to accomplish, not the clicks and selects that the user needs to do to perform these tasks. If you follow the techniques we learned in the previous chapters, which focus on describing business behavior and goals, writing good-quality scenarios comes much more naturally.

Gherkin 场景旨在说明业务规则的示例以及应用程序在特定情况下的预期行为。它们描述了在实现某项功能之前应用程序应该做什么。例如,前面的步骤可能是以下业务规则的一部分:“当用户在给定位置搜索酒店时,系统应显示在请求的日期有空房且距离市中心不超过请求距离的所有酒店。”

Gherkin scenarios are intended to illustrate examples of business rules and of how the application is expected to behave in certain circumstances. They describe what the application should do, before a feature is implemented. For example, the previous steps might be part of the following business rule: “When a user searches for hotels in a given location, the system should show all the hotels that have availability on the requested dates and that are no more than the requested distance from the city center.”

前面所示的步骤的更具说明性的步骤可能是

A more declarative equivalent of the steps shown earlier might be

  我寻找具有以下特征的酒店时:
    | 城市 | 入住日期 | 晚数 | 距离中心距离 |
    | 巴黎 | 2019 年 4 月 4 日 | 2 | 10 公里 |
  When I look for a hotel with:
    | City   | Check-in Date | Nights | Distance from center |
    | Paris  | 04-04-2019    | 2      | 10 km                |

甚至

Or even

  我在 2019 年 4 月 4 日寻找距离巴黎 10 公里以内的 2 晚酒店时
  When I look for a hotel within 10 km of Paris for 2 nights on 04-04-2019

请注意,我们关注的是用户试图做什么,而步骤实现则负责如何完成。这就像经理告诉她的团队需要完成哪些任务,然后让团队决定他们需要做什么才能完成任务。用这种风格编写的场景往往更灵活,更易于阅读和维护。

Notice how we focus on what the user is trying to do and leave the step implementation to worry about how it does so. It’s like the manager telling her team what tasks need to be done and leaving the team to decide what they need to do to make it happen. Scenarios written in this style tend to be more flexible and easier to read and to maintain.

命令式的 Gherkin 有点像一位对团队进行微观管理的经理,不仅告诉他们需要完成什么任务,还向他们提供详细的说明,告诉他们如何完成任务。这给经理带来了很多额外的工作,而团队成员也没有太多自由来决定完成每项任务的最佳方式。

Imperative-style Gherkin is a bit like a manager who micromanages her team, not just telling them what task they need to complete, but giving them detailed, prescriptive instructions on how to do so. It’s a lot of extra work for the manager, and the team members don’t have much freedom to decide for themselves the best way to complete each task.

声明式风格更像是经理解释最终目标是什么;她的员工需要想出实现目标的最佳方法。在 Gherkin 中,声明式风格在自动化场景方面为我们提供了更大的灵活性。例如,酒店搜索步骤可以与网页、Web 服务甚至系统域模型交互。从场景的角度来看,如何实施该步骤对业务的有效性没有影响规则。

A declarative style is more like when a manager explains what the end goal is; it’s up to her people to come up with the best way of accomplishing it. In Gherkin, a declarative style gives us more flexibility when it comes to automating the scenario. For example, the hotel search step could interact with a web page, with a web service, or even with the system domain model. From the point of view of the scenario, how the step is implemented should make no difference to the validity of the business rule.

7.6.4 好的场景只做一件事,并做好这件事

7.6.4 Good scenarios do one thing, and one thing well

在上一节中,我们将一组六个低级交互步骤转换为更以业务为中心的步骤,以捕获用户真正想要做的事情:

In the previous section we transformed a group of six low-level interaction steps into a more business-centric one that captures what the user is really trying to do:

我在 2019 年 4 月 4 日寻找距离巴黎 10 公里以内的 2 晚酒店时
When I look for a hotel within 10 km of Paris for 2 nights on 04-04-2019

然而,当我们把这些小步骤放在一起时,我们意识到这里的业务逻辑实际上相当复杂。我们同时按距离搜索并检查酒店空房情况,这实际上是两个独立的问题。举例说明这一点并不一定是坏事;我们最终需要做到这一点。但如果这是该功能中的第一个场景,它可能会使阅读变得更加困难。

However, when we bring these smaller steps together, we come to realize that the business logic here is actually quite involved. We are simultaneously searching by distance and checking hotel availability, which are really two separate concerns. It is not necessarily a bad thing to illustrate such an example; we will need to get there eventually. But it might make it harder to read if this is the first scenario in the feature.

如果场景从简单情况开始,然后逐步发展为更复杂的情况,那么特征往往更容易理解。如果我们先从单一问题开始,例如按距离搜索,我们的场景将更容易理解:

Features tend to be easier to understand if the scenarios start out with simple cases and build up progressively to more complex ones. Our scenario would be easier to follow if we were to start with a single concern first, such as searching by distance:

我寻找距离巴黎 10 公里内的酒店时
When I look for a hotel within 10 km of Paris

一旦我们确定这个案例正确运行,我们就可以探索涉及位置和可用日期的更复杂的场景。

Once we have established that this case works correctly, we can explore more complex scenarios involving locations and availability dates.

提示:好的场景专注于测试单个业务规则。如果业务规则很复杂,或者场景太大且难以阅读,那么一个好办法就是将场景分解为更小、更集中的场景,以测试规则的特定方面。

TIP Good scenarios focus on testing a single business rule. If a business rule is complex, or if a scenario gets too big and hard to read, a good trick is to break the scenario into smaller, more focused ones that test a specific aspect of the rule.

一般来说,如果场景专注于一条业务规则,那么它们会更容易理解、更容易维护、更容易排除故障。清单 7.4 中的场景没有专注于一条规则,这是它难以理解的原因之一。该场景没有专注于用户使用各种条件搜索酒店时会发生什么,而是继续预订房间、启动付款流程(大概是为了检查正确的总价),然后退出应用程序:

Scenarios in general are easier to understand, easier to maintain, and easier to troubleshoot if they focus on a single business rule. The scenario in listing 7.4 does not focus on a single rule, which is one of the reasons it is hard to follow. Instead of concentrating on what happens when a user searches for a hotel with various criteria, the scenario proceeds to book a room, start the payment process (presumably to check the correct total price), and then log out of the application:

  我点击“搜索”按钮
时  那么列出的酒店是:
    | 酒店名称 | 距离 | 空房情况 | 价格 | 游泳池 | 健身房 |
    |丽兹 | 3.2 |是的 | 400 |是 |是 |
    |萨沃伊 | 6.9 | 6.9是的 | 500 | 500尼 |是 |
  我从列表中选择“Ritz”时
  然后我选择“使用积分支付”              
  检查我是否有足够的积分        
  然后我点击“支付”
  然后验证出现的消息       
  确认价格正确
  然后我点击注销
  When I hit "search" button
  Then the hotels listed are:
    | Hotel Name | Distance | Availability | Price | Pool | Gym |
    | Ritz       | 3.2      | Yes          | 400   | Y    | Y   |
    | Savoy      | 6.9      | Yes          | 500   | N    | Y   |
  When I select "Ritz" from the list
  And I select "Pay with points"             
  And check that I have enough points        
  And I click "Pay"
  Then verify the message that appears       
  And verify that the price is correct
  And I click on logout

选择特定的付款方式

Selects a specific payment option

进行验证

Performs a verification

进行另一次验证

Performs another verification

这是手动或所谓的“端到端”测试脚本的典型特征。它试图将尽可能多的支票塞进一个场景中。但阅读该场景提出的问题远多于它给出的答案。我还可以有哪些其他付款方式,它们会如何改变场景的结果?我需要支付多少积分,积分和价格之间有什么关系?应该出现什么消息,为什么它很重要?等等。

This is typical of manual or so-called “end-to-end” test scripts. It tries to cram as many checks as it can into a single scenario. But reading the scenario raises far more questions than it answers. What other payment options could I have, and how would they alter the outcome of the scenario? How many points do I need to pay, and what is the relationship between points and the price? What is the message that should appear, and why is it significant? And so on.

更易读的方法是一次只关注一条规则。例如,以下场景专注于按距离寻找酒店:

A more readable approach would be to focus on a single rule at a time. For example, the following scenario focuses on looking for hotels by distance:

场景:按距离搜索可用的酒店
  鉴于以下酒店:
    | 酒店名称 | 位置 | 距离中心 |
    | 丽兹酒店 | 巴黎 | 3.2 |
    | 萨沃伊 | 巴黎 | 6.9 |
    | 希尔顿 | 巴黎 | 12.5 |
  我搜索距离巴黎 10 公里内的酒店
时  那么我应该看到以下酒店:
    | 酒店名称 | 位置 | 距离中心 |
    | 丽兹酒店 | 巴黎 | 3.2 |
    | 萨沃伊 | 巴黎 | 6.9 |
Scenario: Search for available hotels by distance
  Given the following hotels:
    | Hotel Name | Location | Distance from center |
    | Ritz       | Paris    | 3.2                  |
    | Savoy      | Paris    | 6.9                  |
    | Hilton     | Paris    | 12.5                 |
  When I search for a hotel within 10 km of Paris
  Then I should be presented with the following hotels:
    | Hotel Name | Location | Distance from center |
    | Ritz       | Paris    | 3.2                  |
    | Savoy      | Paris    | 6.9                  |

其他场景将探索有关房间可用性、付款方式等的业务规则。

Other scenarios would explore business rules around room availability, payment options, and so forth.

在您的场景中提供一些完整的用户旅程示例会很有用。但是,这些示例通常是流程的高级示例,不会详细介绍每个步骤。此类高级场景的示例如下所示:

It can be useful to have a few examples of complete user journeys in your scenarios. However, these will typically be high level examples of flows, which do not go into the details of each step. An example of this type of high-level scenario is shown:

  场景:预订商务旅行
    鉴于Bindi 需要去巴黎出差
    她在公司预订系统上预订航班和酒店时
    那么这次旅行应该放在她的日历上
    的公司信用卡应该被扣款
  Scenario: Booking a business trip
    Given Bindi needs to make a business trip to Paris
    When she books a flight and a hotel on the corporate booking system
    Then the trip should be placed in her calendar
    And her corporate credit card should be charged

大多数场景将重点探索特定业务的示例和变化規則。

The majority of the scenarios will focus on exploring examples and variations of specific business rules.

7.6.5 好的场景有有意义的参与者

7.6.5 Good scenarios have meaningful actors

做过你注意到我们在最后一个场景中是如何从第一人称(“我”)切换到第三人称(“Bindi”)的吗?这是有意为之,并且通常可以使场景更具可读性。

Did you notice how we switched from the first person (“I”) to the third person (“Bindi”) in that last scenario? This was quite intentional, and generally makes for more readable scenarios.

如果你曾经在用户体验(UX)设计团队中工作过中,您可能遇到过“角色”这个术语。在用户体验中,角色是系统典型用户的详细图解示例。特定产品通常有多个角色。对于在线旅行计划器,您可能有吉尔(Jill),她是一名区域总监,每周都要旅行,但对计划相对简单的行程所花的时间感到沮丧(图 7.11)。或者,对于网上银行应用程序,您可能有艾尔莎(Elsa),她是一名总是在路上的年轻高管,需要快速查看自己想要的信息。您可能有山姆(Sam),他是一名家庭主夫,用笔记本电脑查阅账目,密切关注开支,希望查看账户中发生的所有事情的所有详细信息。

If you have ever worked in a team practicing User Experience (UX) design, you will probably have come across the term persona. In UX, a persona is a detailed, illustrated example of a typical user of your system. There are usually several personas for a given product. For an online travel planner, you might have Jill, the regional director, who travels every week and is frustrated at how long it takes her to plan relatively simple trips (figure 7.11). Or, for an online banking application, you might have Elsa, the young executive who is always on the go, and who needs to see the information she wants fast. And you might have Sam, the stay-at-home dad who consults his accounts on his laptop, keeps a close eye on his expenses, and who wants to see all the details of everything that goes on in his account.

图7.11 Jill的角色海报6

Figure 7.11 Jill’s persona poster6

设计师花费大量精力来编写逼真的角色。它们不仅仅是通用的用户角色;它们是丰富的描述,包括目标、能力和背景信息。对于网上银行应用程序,Elsa 和 Sam 的描述还可以包括有关工作、收入和消费习惯的详细信息。

Designers spend a lot of effort in writing realistic personas. They aren’t simply generic user roles; they are rich descriptions, including goals, abilities, and background information. For the online banking application, the descriptions of Elsa and Sam could also include details about jobs, income, and spending habits.

设计师和开发团队通常使用角色来构建能够更好地满足真实用户需求的应用程序。如果您能想象出将使用您功能的人,那么就更容易了解哪些功能对他们有用。

Designers, and development teams in general, use personas to build applications that will respond better to the needs of real-world users. If you can imagine the person who will be using your feature, it is easier to get a feel for what might be useful for them.

用户故事中的角色

Personas in User Stories

这时,用户故事就派上用场了。许多团队编写的用户故事都是针对那些不露面的普通用户,并给他们起一些平淡无奇的头衔,比如“用户”或“客户”。

This is where User Stories come into the picture. Many teams write User Stories with faceless, generic users, giving bland titles like “The User” or “The Customer.”

作为用户

As a user

我想查看我的帐户详情

I want to see my account details

这样我就能知道我的账户里有多少钱

So that I can know how much money I have in my account

这个故事的第一行告诉我们的信息很少,也没有给我们任何线索来说明用户可能正在寻找什么。但是当我们将角色引入这些对话时,用户故事就变得有趣多了。假设我们从 Elsa 的角度来讲述这个故事:

The first line of this story tells us very little and gives us no clues as to the finer points of what the user might be looking for. But when we introduce personas into these conversations, the User Stories become much more interesting. Suppose we present this story from the point of view of Elsa:

艾尔莎,忙碌的年轻高管

Elsa, the young executive on the go

想要一目了然地查看账户余额

Wants to view her account balance at a glance

这样她就能快速知道是否需要从储蓄账户中转账

So that she can quickly know whether she needs to transfer money from her savings account

这个故事给了我们更多关于艾莎可能喜欢的设计的提示。查看用户帐户详细信息的方法有很多,但艾莎肯定更喜欢在主页上以显眼的字体看到她的主要帐户的数据,而不是点击三下后出现在页面左上角。屏幕。

This story gives us many more hints about the sort of design Elsa might like. There are many ways to view a user’s account details, but Elsa would surely prefer to see the figures of her primary accounts in a prominent font on the home page, rather than three clicks away and in the top-left corner of the screen.

在场景中使用角色

Using personas in scenarios

角色这对于验收标准也非常有用。它们帮助我们更严格地审视场景,以评估它们是否真的能为用户带来价值。

Personas work well for acceptance criteria as well. They help us look at scenarios more critically, to assess whether they would really deliver value to our users.

以以下场景为例:

Take the following scenario, for example:

  场景:账户主页
    鉴于我有一个储蓄账户
    我打开我的账户主页
时    然后我应该看到有关我的帐户的详细信息
  Scenario: Account home page
    Given I have a savings account
    When I open my accounts home page
    Then I should see details about my account

这感觉有点模糊。我们没有描述哪些具体的帐户详细信息是相关的,这给人留下了很多想象空间。但是,虽然有选择可探索是件好事,但设计过程需要约束和指导才能产生良好的结果。

This feels a tad vague. We don’t describe what specific account details would be relevant, which leaves a lot to the imagination. But while having options to explore is a good thing, a design process needs constraints and guidelines to produce good results.

现在假设我们从 Sam 的角度考虑这种情况。Sam 非常关注自己的开支。因此,他会想知道哪些细节。从 Sam 的角度来看,Sam 真正感兴趣的是知道他还剩多少钱可以花,而且他知道账户的当前余额通常并不总是反映实际可用金额。

Now suppose we consider this scenario from the point of view of Sam. Sam watches his expenses like a hawk. So, he will want to know which details. From Sam’s perspective, Sam is really interested in knowing how much money he has left to spend, and he knows that often the current balance of an account doesn’t always reflect the actual amount available.

考虑到这一点,我们可以编写这样的场景:

Taking this into consideration, we could write a scenario like this:

  场景:账户所有者应该能够一目了然地看到自己的余额
    假设Sam 有以下帐户:
      | 类型 | 数量 | 当前余额 | 待处理交易 |
      | 当前 | 123456 | $530.00 | $-200.00 |
      | 节省 | 234567 | 2500 美元 | |
    他查看自己的帐户摘要时
    然后他应该看到每个账户的余额和待处理交易 
帐户:
      | 类型 | 当前余额 | 待处理交易 | 可用 |
      | 当前 | $530.00 | $-200.00 | $330.00 |
      | 节省 | 2500 美元 | | 2500.00 美元 |
  Scenario: Account owners should be able to see their balance at a glance
    Given Sam has the following accounts:
      | Type    | Number | Current Balance | Pending Transaction |
      | Current | 123456 | $530.00         | $-200.00            |
      | Savings | 234567 | $2500           |                     |
    When he views his account summary
    Then he should see the balance and pending transactions for each 
 account:
      | Type     | Current Balance | Pending Transactions | Available |
      | Current  | $530.00         | $-200.00             | $330.00   |
      | Savings  | $2500           |                      | $2500.00  |

注意看第二个例子有多丰富?它不仅提供了更多关于哪些信息相关且有用的详细信息,而且直接解决了 Sam 的目标担忧。

Notice how much richer this second example is? Not only does it give more details about what information is relevant and useful, but it directly addresses Sam’s goals and concerns.

肥皂剧角色

Soap opera personas

角色显然是一个强大的工具。然而,许多团队并没有从这个意义上使用角色。许多团队没有专门的用户体验活动,也没有时间投入到详细的用户研究。如果你属于这一类,不要担心!这不会阻止你有效地利用演员。如果你手头没有任何成熟的角色,敏捷教练 Andy Palmer 建议使用肥皂剧角色的想法。a你不必花很多时间预先定义你的角色,只需在编写故事时随时介绍它们即可。

Personas are clearly a powerful tool. However, many teams don’t work with personas in this sense. Many teams do not have a dedicated UX activity, or the time to invest in detailed user research. If you are in this category, do not be troubled! This won't stop you from making effective use of actors. If you don't have any full-blown personas handy, Agile coach Andy Palmer recommends using the idea of soap opera personas.a Rather than spending a lot of time upfront defining your personas, you simply introduce them on the go, as you write the stories.

这个想法来自电视肥皂剧。当你在 Netflix 上观看新剧集时,你不会看到每个新角色的详细介绍。最多,你可能会听到旁白的简短介绍,或者主角的尖刻评论:“这是我姐夫比尔。他讨厌我。”

The idea comes from TV soap operas. When you watch a new series on Netflix, you don't get a detailed presentation of each new character. At best, you might get a short introduction from a narrator or a snarky comment from the main character: “This is my brother-in-law, Bill. He hates my guts.”

相反,你可以从简单的角色开始,比如“合规官凯莉”或“小企业主巴里”。随着故事的发展,我们会更多地了解凯莉和巴里。例如,巴里有一个企业账户,居住在百慕大,这将使他受到合规办公室的额外关注。或者我们可能会发现不符合我们现有场景的场景,比如高级分析师桑德拉,她需要审查凯莉没有资格处理的案件。

Instead, you start with simple characters, like “Carrie, the compliance officer” or “Barry, the small business owner.” As the stories develop, we will learn more about Carrie and Barry. For example, Barry has a business account and is domiciled in Bermuda, which will subject him to extra attention from the compliance office. Or we might discover scenarios that don’t fit any of our existing scenarios, such as Sandra the senior analyst, who needs to review cases that Carrie is not qualified to handle.


一个  Andy Palmer,《肥皂剧人物》,2014 年 3 月 8 日,http://andypalmer.com/2014/03/soap-opera-personas/

a  Andy Palmer, “Soap Opera Personas,” March 8, 2014, http://andypalmer.com/2014/03/soap-opera-personas/

7.6.6 好的场景聚焦本质,隐藏次要

7.6.6 Good scenarios focus on the essential and hide the incidental

小黄瓜场景都是关于沟通的——业务人员和开发团队之间的沟通,以及开发团队成员之间的沟通。每个场景都有一个信息,一个故事要讲。然而,当一个场景详细描述每个步骤时,很容易忽略实际的信息。这使得进行对话或提供有关业务的有用反馈变得更加困难。

Gherkin scenarios are all about communication—communication between businesspeople and the development team, and communication between members of the development team. Each scenario has a message, a story to tell. However, when a scenario describes each step in detail, it is easy to lose track of the actual message. And this makes it harder to have conversations or give useful feedback about the business.

例如,清单 7.4 中的场景的前五行如下:

For example, the first five lines of the scenario in listing 7.4 read as follows:

假设我登录了旅行预订应用程序 
然后我就在主页上 
然后我点击“新行程”标签
然后我点击“酒店”下拉菜单项
然后我就进入了酒店搜索页面
Given I login to the Travel Booking App 
Then I am on the Home Page 
And I click the New Trip tab
And I click on the Hotels dropdown menu item
Then I am on the Hotel Search page

如果我们的场景是根据空房情况和与市中心的距离来搜索酒店,我们可以假设我们知道如何登录应用程序。因此,可以安全地假设第一步并在后台实施。

If our scenario is about searching for hotels based on availability and distance from the town center, we can assume that we know how to log on to the application. So, the first step can safely be assumed and implemented behind the scenes.

以下四个步骤描述了从主页到酒店搜索页面的导航。但此场景的主题不是导航(导航可能会发生变化;如果用户有一个书签可以直接将她带到搜索页面,会怎么样?这会改变业务逻辑吗?)。此场景的主题是关于您进入酒店搜索页面后如何搜索酒店。所有这些步骤都可以在一个步骤中恢复,从而使应用程序达到所需状态,例如:

The following four steps describe the navigation from the home page to the Hotel Search page. But the subject of this scenario is not about navigation (which could change; what if the user has a bookmark that takes her directly to the search page? Would this change the business logic?). The subject of this scenario is about how you search for a hotel, once you are on the Hotel Search page. All these steps could be resumed in a single step that would bring the application to the desired state, such as the following:

  鉴于Bindi 需要去巴黎出差
  Given Bindi needs to make a business trip to Paris

但是,不相关和过于详细的步骤并不是我们发现偶然细节的唯一地方。例如,Then清单 7.4 中的步骤描述了 Bindi 应该看到的酒店:

But irrelevant and overly detailed steps are not the only place we find incidental detail. For example, the Then step in listing 7.4 describes the hotels that Bindi should see:

  那么列出的酒店是:
    | 酒店名称 | 距离 | 空房情况 | 价格 | 游泳池 | 健身房 |
    |丽兹 | 3.2 |是的 | 400 |是 |是 |
    |萨沃伊 | 6.9 | 6.9是的 | 500 | 500是 |是 |
  Then the hotels listed are:
    | Hotel Name | Distance | Availability | Price | Pool | Gym |
    | Ritz       | 3.2      | Yes          | 400   | Y    | Y   |
    | Savoy      | 6.9      | Yes          | 500   | Y    | Y   |

此步骤包含大量信息,其中大部分与我们正在研究的场景无关。就此场景(重点是按距离搜索)而言,我们确实不需要有关游泳池和健身房等酒店设施的详细信息。价格也不相关,因为我们对本场景的付款不感兴趣。可用性很重要,但可以假设所有酒店都具有可用性。

This step contains a lot of information, much of which is not relevant to the scenario we are looking at. For the purposes of this scenario (which focus on searching by distance), we really don’t need details about hotel facilities such as pool and gym. Nor is the price relevant, as we are not interested in payment for this scenario. The availability is important but could be assumed to be true for all hotels for the purpose of this scenario.

一旦我们删除所有这些,我们将得到一个更加简洁、更集中的表格,其中概括了读者需要知道的基本信息:

Once we remove all this, we are left with a much more concise and focused table that resumes the essential information that the reader needs to know:

  那么列出的酒店是:
    | 酒店名称 | 距离 | 
    | 里兹 | 3.2 | 
    | 萨沃伊 | 6.9 |
  Then the hotels listed are:
    | Hotel Name | Distance | 
    | Ritz       | 3.2      | 
    | Savoy      | 6.9      | 

我们仍然缺少一些重要信息:这些酒店来自哪里?为什么特别选择这些酒店?现实世界中的 Gherkin 场景经常陷入依赖生产数据或类似生产数据的陷阱。

We are still missing some important information: Where do these hotels come from? Why these hotels in particular? Real-world Gherkin scenarios often fall into the trap of relying on production or production-like data.

Given好的 Gherkin 场景从第一个语句开始就具有逻辑性进入决赛When,其中包括描述和设置场景运行所需的任何初始数据。在本例中,我们可以在 Bindi 执行搜索之前描述已知的酒店,步骤如下:

Good Gherkin scenarios show a logical progression from the first Given statement to the final When, and this includes describing and setting up any initial data that the scenario needs to work. In this instance, we could describe the known hotels before Bindi performs her search, with a step like the following:

  鉴于以下酒店有空房:
    | 酒店名称 | 位置 | 距离中心 |
    | 丽兹酒店 | 巴黎 | 3.2 |
    | 萨沃伊 | 巴黎 | 6.9 |
    | 希尔顿 | 巴黎 | 12.5 |
  Given the following hotels have available rooms:
    | Hotel Name | Location | Distance from center |
    | Ritz       | Paris    | 3.2                  |
    | Savoy      | Paris    | 6.9                  |
    | Hilton     | Paris    | 12.5                 |

提示:编写良好的场景既描述行为也描述数据。当场景使用数据来说明行为时(例如在这些与搜索相关的场景中),它应该描述初始状态和最终状态,并管理或设置测试数据以确保系统处于预期的初始状态。

TIP Well-written scenarios describe both behavior and data. When a scenario uses data to illustrate behavior (such as in these search-related scenarios), it should describe the initial state and the final state and manage or set up the test data to ensure that the system is in the expected initial state.

使用我们目前所见的技术,原始场景的前半部分可以重写如下:

Using the techniques we have seen so far, the first half of our original scenario could be rewritten as follows:

场景:按距离搜索可用的酒店
  鉴于Bindi 需要去巴黎出差
  以下酒店都有空房:
    | 酒店名称 | 位置 | 距离中心 |
    | 丽兹酒店 | 巴黎 | 3.2 |
    | 萨沃伊 | 巴黎 | 6.9 |
    | 希尔顿 | 巴黎 | 12.5 |
  Bindi 搜索距离巴黎 10 公里内的酒店
时  那么她应该被推荐以下酒店:
    | 酒店名称 | 位置 | 距离中心 |
    | 丽兹酒店 | 巴黎 | 3.2 |
    | 萨沃伊 | 巴黎 | 6.9 |
Scenario: Search for available hotels by distance
  Given Bindi needs to make a business trip to Paris
  And the following hotels have available rooms:
    | Hotel Name | Location | Distance from center |
    | Ritz       | Paris    | 3.2                  |
    | Savoy      | Paris    | 6.9                  |
    | Hilton     | Paris    | 12.5                 |
  When Bindi searches for a hotel within 10 km of Paris
  Then she should be presented with the following hotels:
    | Hotel Name | Location | Distance from center |
    | Ritz       | Paris    | 3.2                  |
    | Savoy      | Paris    | 6.9                  |

附带细节也经常出现在示例表中。例如,以下场景描述了新常旅客会员如何从过去三个月乘坐的航班中赚取积分:

Incidental detail often slips into example tables as well. For example, the following scenario describes how new Frequent Flyer members can earn points from flights that they took in the three previous months:

  场景概述:通过以往航班赚取飞行常客积分
    鉴于Terry 于 2019 年 1 月 20 日加入了飞行常客计划
    他要求将此航班计入他的飞行常客积分时 
點:
    | 预订参考 | <预订> |
    | 航班号 | <号码> |
    | 航班日期 | <日期> |
    | 航空公司 | <航空公司> |
    | 出发 | <从> |
    | 目的地 | <至> |
    然后航班应该记入:<Credit>
  例如    | 预订 | 编号 | 日期 | 航空公司 | 从 | 至 | 信用额度 |
    | DDSF245 | FH-101 | 2018 年 12 月 20 日 | 飞得高 | 伦敦 | 巴黎 | 是 |
    | SFGG345 | FH-201 | 2018 年 10 月 21 日 | 飞得高 | 伦敦 | 奥斯陆 | 是 |
    | DDSF245 | FH-092 | 2018 年 11 月 20 日 | 飞得高 | 伦敦 | 巴黎 | 是 |
    | SFGG345 | FH-999 | 2018 年 5 月 25 日 | 飞得高 | 伦敦 | 柏林 | 否 |
    | KEGR264 | OA-102 | 20-12-2018 | 其他航空 | 巴黎 | 马德里 | 否 |
  Scenario Outline: Earning Frequent Flyer points for past flights
    Given Terry joined the Frequent Flyer program on 20-01-2019
    When he asks for this flight to count towards his Frequent Flyer 
 points:
    | Booking Reference | <Booking> |
    | Flight Number     | <Number>  |
    | Flight Date       | <Date>    |
    | Airline           | <Airline> |
    | Departure         | <From>    |
    | Destination       | <To>      |
    Then the flight should be credited: <Credit>
  Examples:
    | Booking | Number | Date       | Airline     | From   | To     | Credit |
    | DDSF245 | FH-101 | 20-12-2018 | Flying High | London | Paris  | Yes    |
    | SFGG345 | FH-201 | 21-10-2018 | Flying High | London | Oslo   | Yes    |
    | DDSF245 | FH-092 | 20-11-2018 | Flying High | London | Paris  | Yes    |
    | SFGG345 | FH-999 | 25-05-2018 | Flying High | London | Berlin | No     |
    | KEGR264 | OA-102 | 20-12-2018 | Other Air   | Paris  | Madrid | No     |

此场景包含大量不必要的附带信息,这让实际业务逻辑更难辨别。数据行数很多,但无法立即看出为什么有些数据被记入贷方,而有些数据没有。此场景还缺少一些可以使其更容易理解的细节。

This scenario has a lot of unnecessary incidental information, which makes it harder to discern the actual business logic. There are many rows of data, but it is not immediately obvious why some are credited and some are not. The scenario is also missing a few details that would make it easier to digest.

一般来说,表格应该只包含与所述业务规则直接相关的信息。例如,预订参考对航班是否记入账户没有直接影响,因此这些信息可以隐藏在实施细节中。

Generally, a table should only include information that is directly relevant to the business rule being described. For example, the booking reference has no direct affect on whether a flight will be credited or not, so this information could be hidden in the implementation details.

一个有用的经验法则是质疑任何值相同或对场景结果没有明显影响的列的值。例如,在表中,出发城市和目的地城市与航班是否应计入新会员的飞行常客积分无关。当要了解获得多少积分时,这些信息很重要,但这不是本场景的主题。

One useful rule of thumb is to question the value of any column whose values are identical or have no clear influence on the outcome of the scenario. For example, in the table, the departure and destination cities have no bearing on whether a flight should count toward the new member’s Frequent Flyer points. This information will be important when it comes to knowing how many points were earned, but that is not the topic of this scenario.

尽管航班号可能看起来像是一个无关紧要的细节,但我们决定保留它,因为 Flying High Airlines 的员工都非常熟悉它。在从场景中删除无关紧要的细节时,重要的是要记住,我们需要删除足够多的细节,以便场景专注于我们想要强调的事情,但不要删除太多,以免变得过于笼统和模糊。

Although the flight number might seem like an incidental detail, we’ve decided to keep it as people working for Flying High Airlines are intimately familiar with them. When removing incidental detail from a scenario it’s important to remember that we need to remove enough of it so that the scenario is focused on the things we want to highlight, but not too much, so that it doesn’t become overly generic and vague.

我们还可以在表格中或场景本身中添加额外信息,以提供有关示例的更多线索。在以下版本中,我们添加了注释列,为读者提供有关每个示例所应用的业务规则的一些线索

We can also add extra information in a table, or in the scenario itself, to give a few more clues about the examples. In the following version, we have added a Notes column to give readers some clues about the business rule being applied for each example.

场景概述:通过以往航班赚取飞行常客积分
用户可以申请过去完成的常旅客航班积分 
三个月
  鉴于Terry 于 2019 年 1 月 20 日加入了飞行常客计划
  Terry 申请 <日期> 的 <航空公司> 航班 <编号> 积分时
  然后该航班应该被记入:<已记入>
  例如    | 编号 | 日期 | 航空公司 | 贷记 | 注释 |
    | FH-101 | 2019 年 1 月 2 日 | 飞得高 | 是 | |
    | FH-999 | 2018-10-20 | 飞得高 | 否 | 飞行时间太长 |
    | OA-102 | 01-02-2019 | 其他航空 | 否 | 不适用于 Flying High |
Scenario Outline: Earning Frequent Flyer points for past flights
Users can request credits for Frequent Flyer flights completed in the past 
 three months
  Given Terry joined the Frequent Flyer program on 20-01-2019
  When Terry requests credit for flight <Number> on <Date> with <Airline>
  Then the flight should be credited: <Credited>
  Examples:
    | Number | Date       | Airline     | Credited | Notes                |
    | FH-101 | 01-02-2019 | Flying High | Yes      |                      |
    | FH-999 | 20-10-2018 | Flying High | No       | Flight too old       |
    | OA-102 | 01-02-2019 | Other Air   | No       | Not with Flying High |

卖鱼人的故事

The story of the fish seller

消除附带性有点像那个关于卖鱼人的古老笑话。a繁忙市场上的卖鱼人刚刚画了一块新招牌。上面写着:“这里卖新鲜鱼。

Eliminating the incidental is a bit like the old joke about the fish seller.a A fish seller in a busy marketplace has just painted a new sign. It reads: “Fresh fish sold here.”

一位路过的朋友注意到了这个招牌,并指出,“你们卖烂鱼吗?”于是卖鱼的人将招牌改为“此地卖鱼”。

A friend passing by notices the sign, and points out, “Would you sell rotten fish?” So the fish seller changes the sign to “Fish sold here.”

附近的另一位朋友看到了新招牌,说道:“你干嘛费心写‘卖’?没人指望你把它们免费赠送。”于是他再次更新了招牌,改为“在此钓鱼”。

Another friend in the vicinity sees the new sign and says, “Why do you bother saying ‘sold’? No one expects you to give them away.” So he updates the sign once more, to “Fish here.”

当天晚些时候,另一个朋友过来了。他看了看招牌,问道:“为什么写着‘这里’?它们还能在哪里呢?”于是卖家将招牌换成了一个词:“鱼”。

Later in the day, another friend comes by. He reads the sign and asks, “Why say ‘here’? Where else would they be?” So the seller replaces his sign with a single word: “Fish.”

最后,另一个朋友路过并看到了这个招牌。他很有逻辑地说:“但每个人都能看到你卖鱼!”于是招牌上剩下的最后一个字就消失了。

Finally, another friend passes by and sees the sign. He says, quite logically, “But everyone can see you sell fish!” So the last remaining word on the sign disappears.


一个  这个故事至少可以追溯到 1890 年,当时它被刊登在《纽黑文纪事报》上。

a  This story dates back to at least 1890, when it was printed in the New Haven Register.

7.6.7 Gherkin 场景不是测试脚本

7.6.7 Gherkin scenarios are not test scripts

一个对于刚接触 BDD 的团队来说,常见的做法是将传统的测试脚本或手动测试用例转换为 Gherkin 场景。这通常不是一个好主意。

A common practice for teams new to BDD is to transform traditional test scripts or manual test cases into Gherkin scenarios. This is generally not a good idea.

手动测试与自动测试的工作原理截然不同。在手动测试脚本中,您通常会尝试在一次通过中完成尽可能多的测试,因为设置环境和逐步完成屏幕需要时间。您希望充分利用每个屏幕上的时间来尽可能多地进行测试。

Manual testing works very differently than automated testing. In a manual test script, you often try to do as much testing as possible in a single pass, because it takes time to set up the environment and step through the screens. You want to make the most of the time on each screen to test as much as you can.

但这对于自动验收测试或 BDD 场景来说并不是一个好策略。精心设计的自动验收测试比手动测试运行得快得多,但要想发挥作用,它们需要在出现问题时提供准确的反馈。BDD 场景使用示例和反例来探索业务规则,使用业务人员可以理解并给出有用反馈的语言。专注于单一流程的长测试脚本使这些对话变得更加困难,并且更容易错过重要的边缘情况或变化。

But this isn’t a good strategy for automated acceptance tests or BDD scenarios. Well-designed automated acceptance tests run much more quickly than a manual test, but to be useful they need to give precise feedback when something goes wrong. BDD scenarios explore business rules using examples and counterexamples, in a language that businesspeople can understand and give useful feedback on. Long test scripts that focus on a single flow make it much harder to have these conversations and make it easier to miss important edge cases or variations.

很容易发现已自动化的手动测试脚本。它们通常具有测试脚本样式的名称,例如“端到端酒店预订测试”或“TC1-酒店预订-正面测试用例”。或者它们以命令式风格编写(“检查我是否有足够的积分”或“验证出现的消息”),就好像它们是对人类测试人员的指示一样。测试脚本场景也往往由一长串低级或 UI 交互组成,其中混合了GivenWhenThen语句没有特定的顺序。它们通常描述测试人员应该做什么(“验证出现的消息”),但没有描述实际的预期结果。

It is easy to spot manual test scripts that have been automated. They often have test-script style names like “End-to-end Hotel Booking Test” or “TC1–Hotel Booking–positive test case.” Or they are written in the imperative style (“Check that I have enough points” or “Verify the message that appears”), as if they are instructions to a human tester. Test script scenarios also tend to be made up of long lists of low-level or UI interactions, with a mixture of Given, When, and Then statements in no particular order. They often describe what the tester should do (“Verify the message that appears”) but without describing the actual expected outcome.

提示带有“验证”或“检查”等字眼的场景通常是伪装的测试脚本。写得好的场景会以明确的术语描述他们想要实现的结果,而不是向测试人员提供一般性说明。

TIP A scenario with words like “verify” or “check” is often a test script in disguise. Well-written scenarios describe the outcomes they want to achieve in unambiguous terms rather than giving general instructions to a tester.

例如,在清单 7.4 中,我们看到以下步骤:

For example, in listing 7.4, we see the following steps:

当我从列表中选择“Ritz”时
我选择“使用积分支付”
并检查我是否有足够的积分
When I select "Ritz" from the list
And I select "Pay with points"
And check that I have enough points

这些步骤可以作为人工测试人员的说明,他们可以合理地解释它们。但是,对于自动验收测试,“检查我是否有足够的积分”太模糊了。更好的方法是有一个单独的场景来探索使用积分支付的业务规则,例如以下示例:

These steps would work as instructions for a human tester, who could interpret them sensibly. However, for an automated acceptance test, “Check that I have enough points” is far too vague. A better approach would be to have a separate scenario to explore the business rules around paying with points, such as in the following example:

场景:旅行者可以使用积分支付酒店预订费用
常旅客可以使用积分预订房间,每积分 1 美元
  鉴于以下酒店有空房:
    | 酒店名称 | 位置 | 每晚费用 |
    | 里兹 | 纽约 | 500 |
  Bindi的余额为 1200 点
  她在丽兹酒店预订两晚住宿时
  那么她应该被收取 0 美元的剩余余额
  应该还剩下200分
Scenario: Travelers can pay for a hotel booking using points
Frequent Flyers can book rooms using points at a rate of $1 per point
  Given the following hotels have available rooms:
    | Hotel Name | Location | Nightly cost |
    | Ritz       | New York | 500          |
  And Bindi has a balance of 1200 points
  When she books 2 nights at the Ritz
  Then she should be charged a remaining balance of $0
  And she should have 200 remaining points

拥有单独的场景意味着我们可以探索特定业务规则的不同方面和反例,而使用端到端测试脚本则很难做到这一点。例如,我们可以使用场景大纲来探索旅行者使用现金和积分组合支付的不同方式:

Having a separate scenario means we can explore different aspects and counterexamples of a specific business rule, which is much harder to do with an end-to-end test script. For example, we could use a scenario outline to explore different ways a traveler can pay with a combination of cash and points:

场景概述:旅行者可以使用现金和积分支付
积分先消费,后兑现
  鉴于以下酒店有空房:
    | 酒店名称 | 位置 | 每晚费用 |
    | 里兹 | 纽约 | 500 |
  并且Bindi 的余额为 <可用积分> 点
  她在丽兹酒店预订两晚住宿时
  然后她应该被收取剩余金额 $<现金成本>
  应该还剩下<Remaining Points>分
  例如    | 可用积分 | 现金成本 | 剩余积分 |
    | 1200 | 0 | 200 |
    | 800 | 200 | 0 |
    | 0 | 1000 | 0 |
Scenario Outline: Travelers can pay using cash and points
Points are consumed before cash
  Given the following hotels have available rooms:
    | Hotel Name | Location | Nightly cost |
    | Ritz       | New York | 500          |
  And Bindi has a balance of <Available Points> points
  When she books 2 nights at the Ritz
  Then she should be charged a remaining balance of $<Cash cost>
  And she should have <Remaining Points> points remaining
  Examples:
    | Available Points | Cash cost | Remaining Points |
    | 1200             | 0         | 200              |
    | 800              | 200       | 0                |
    | 0                | 1000      | 0                |

好的 Gherkin 场景非常有效地描述业务规则和行为,并探索用户旅程中不同阶段可能发生的情况。总之,以这种方式编写的场景比单一的端到端测试更能完整地描绘用户旅程脚本。

Good Gherkin scenarios are very effective at describing business rules and behavior and exploring what can happen at different stages in a user journey. Together, scenarios written this way give a much more complete picture of the user journey than a single, end-to-end test script.

7.6.8 好的场景是独立的

7.6.8 Good scenarios are independent

作为我们已经看到,像清单 7.4 中那样冗长的场景远非理想。步骤多的场景,尤其是 UI 测试,往往比简短、集中的场景更慢、更脆弱。脆弱的测试容易随机失败,没有明显的原因。长时间运行的 Web 测试特别容易出现这种类型的故障;现代异步应用程序有时很难以可靠的方式自动化,Web 测试有时也会因系统负载或网络问题而失败。

As we have seen, long-winded scenarios like the one in listing 7.4 are far from ideal. Scenarios with many steps, particularly UI tests, tend to be slower and more brittle than short, focused ones. A brittle test is one that is prone to failing randomly, for no apparent reason. Long-running web tests are particularly subject to this type of failure; modern asynchronous applications can sometimes be tricky to automate in a reliable manner, and web tests can sometimes also fail due to system load or network issues.

有时,较长的端到端测试不是以包含许多步骤的单个场景的形式编写的,而是以一系列相关场景的形式编写的,其中每个场景都紧接着前一个场景。下面的清单显示了这种方法的一个示例。

Sometimes long end-to-end tests are written not as a single scenario with lots of steps, but as a sequence of related scenarios, where each scenario follows on from the previous one. An example of this approach is shown in the following listing.

清单 7.5 场景序列

Listing 7.5 A sequence of scenarios

场景:步骤 1 - 用户导航到预订页面
  鉴于我登录了企业预订应用程序
  我点击“酒店”按钮
时  然后我就会看到酒店搜索页面                 
 
场景:步骤 2 - 用户按位置寻找酒店
  假设我在城市字段中输入“巴黎”                
  将价格范围设置为“任意”
  将入住日期设置为“04-04-2019”
  输入的住宿天数为“2”
  然后我点击“确定”
  我应该看看相关的酒店
 
场景:步骤 3 - 用户使用积分和现金支付
  鉴于我选择丽兹酒店                           
  然后我点击使用积分和现金支付
  然后我会看到一个对话框询问我想花多少积分
...                                                        
Scenario: Step 1 - User navigates to the booking page
  Given I log in to the Corporate Booking App
  When I click on the "Hotels" button
  Then I should see the Hotel search page                 
 
Scenario: Step 2 - User looks for a hotel by location
  Given I Enter "Paris" into the city field               
  And I set price range to "Any"
  And I set check-in date to "04-04-2019"
  And I enter number of nights to "2"
  And I click on OK
  Then I should see the relevant hotels
 
Scenario: Step 3 - User pays with points and cash
  Given I select the Ritz hotel                           
  And I click on Pay with Points and Cash
  Then I should see a dialog asking how many points I want to spend
...                                                       

我们希望此场景能将用户带到酒店搜索页面。

We expect this scenario to take the user to the Hotel search page.

进入搜索页面后,用户开始输入搜索条件。

Once on the search page, the user starts entering search criteria.

找到匹配的酒店列表后,选择一家酒店。

Once a list of matching hotels has been found, select a hotel.

更多场景执行其他相关操作。

More scenarios perform other related actions.

第一种场景将用户带到酒店搜索页面,第二种场景搜索给定位置的酒店。只有当第一种场景成功完成时,第二种场景才会起作用。同样,只有当第二种场景成功完成时,第三种场景才会起作用。其他类似的场景将随之而来,以完成用户旅程。

The first scenario takes the user to the Hotel search page, and the second searches for a hotel in a given location. The second scenario will only work if the first one completed successfully. Likewise, the third scenario will only work if the second was successful. Other similar scenarios would follow to complete the user journey.

一系列相关场景当然比单个包含大量步骤的场景更容易阅读,但结果可能同样脆弱。如果一个场景失败,后面的场景也会受到影响,这使得更难清楚地了解应用程序的健康状况。

A series of related scenarios is certainly easier to read than a single scenario with a large number of steps, but the result can be just as fragile. If one scenario fails, the following ones will be compromised, making it harder to get a clear picture of the health of the application.

使用数据处理或消息传递应用程序的团队在尝试模拟类似生产的数据流经过多个处理阶段时经常会遇到类似的问题。测试基础设施可能难以设置和维护,步骤通常运行缓慢,并且如果一个步骤失败,以下场景也会受到影响。

Teams working with data processing or messaging applications often have similar problems when they try to simulate a flow of production-like data through many stages of processing. The testing infrastructure can be hard to set up and maintain, steps are often slow to run, and if one step fails, the following scenarios will also be compromised.

尽管如此,业务人员在描述完整的用户旅程时经常发现这种格式很有用。许多团队喜欢先使用几个端到端场景来提供整体情况,然后再使用更有针对性的场景来探索流程每个阶段的特定业务规则。

Nevertheless, businesspeople often find this format useful when it comes to describing a complete user journey. Many teams like to have a few end-to-end scenarios to give the bigger picture, before using more focused scenarios to explore specific business rules at each stage of the process.

提示请务必记住,即使场景位于同一个功能文件中,每个场景也应是独立的。每个场景都应能够独立工作:场景不应依赖于前一个正在运行的场景来设置数据或使系统进入特定状态。

TIP It’s important to remember that even if the scenarios live together in the same feature file, each scenario should be independent. Each scenario should be able to work in isolation: a scenario should not rely on a previous one running to set up data or to put the system into a particular state.

更好的方法是组织一系列独立的场景,以说明用户旅程的每个步骤。每个场景负责将测试环境置于正确的初始状态,并验证它是否正确完成了流程的这一步。如果一个步骤失败,其他步骤仍可执行,这使得解决问题变得更加容易。以下清单显示了此方法的一个示例。

A better approach is to organize a series of independent scenarios that illustrate each step of a user journey. Each scenario is responsible for putting the test environment into the correct initial state and verifying that it completes this step of the process correctly. If one step fails, the others can still be executed, which makes it easier to troubleshoot issues. An example of this approach is shown in the following listing.

清单 7.6 一系列相关但独立的场景

Listing 7.6 A series of related-but-independent scenarios

场景:旅行者可以根据位置和价格寻找酒店        
  由于Bindi 需要去纽约出差
  以下酒店都有空房:
    | 酒店名称 | 位置 | 每晚费用 | 政策 |
    | 丽兹酒店 | 巴黎 | 400 | 是 |
    | 希尔顿 | 纽约 | 600 | 是 |
    | 康拉德 | 纽约 | 800 | 否 |
  她寻找纽约的酒店
时  然后她应该被引导到以下酒店:    
    | 酒店名称 | 位置 | 每晚费用 | 政策 |
    | 希尔顿 | 纽约 | 600 | 是 |
    | 康拉德 | 纽约 | 800 | 否 |
 
场景:旅行者使用公司信用卡预订酒店
  鉴于Bindi 正在寻找纽约的酒店
  她用公司卡预订纽约希尔顿酒店房间
时  那么预订应该被搁置
  报送后勤部门审批行程计划
 
场景:每次出行都需要经过物流人员的批准      
  鉴于Bindi 已预订了以下行程:
    | 目的地 | 日期 | 夜晚 | 酒店 | 航班 |
    | 纽约 | 2020 年 5 月 15 日 | 3 | 希尔顿 | FH-101 |
  洛根是一名后勤官员
  洛根回顾他的任务清单
时  然后Logan 应该会在收件箱中看到以下行程:
    | 目的地 | 日期 | 预订 | 状态 |
    | 纽约 | 2020 年 5 月 15 日至 18 日 | 航班、酒店 | 待批准 |
 
场景:旅行者可以查看已批准的即将到来的行程        
  鉴于Bindi 已预订了以下行程:
    | 目的地 | 日期 | 夜晚 | 酒店 | 航班 |
    | 纽约 | 2020 年 5 月 15 日 | 3 | 希尔顿 | FH-101 |
  并且此行程已获得物流部门批准
  当她回顾即将到来的旅行时
  那么她接下来的旅行应该包括:
    | 目的地 | 日期 | 预订 | 状态 |
    | 纽约 | 2020 年 5 月 15 日至 18 日 | 航班、酒店 | 已批准 |
Scenario: A traveler can look for a hotel by location and price        
  Given Bindi needs to travel to New York for business
  And the following hotels have available rooms:
    | Hotel Name | Location | Nightly cost | In policy |
    | Ritz       | Paris    | 400          | Yes       |
    | Hilton     | New York | 600          | Yes       |
    | Conrad     | New York | 800          | No        |
  When she looks for hotels in New York
  Then she should be shown the following hotels:    
    | Hotel Name | Location | Nightly cost | In policy |
    | Hilton     | New York | 600          | Yes       |
    | Conrad     | New York | 800          | No        |
 
Scenario: A traveler books a hotel using their corporate credit card
  Given Bindi is looking for hotels in New York
  When she books a room in the New York Hilton with her corporate card
  Then the booking should be placed on hold
  And the travel plan should be submitted to Logistics for approval
 
Scenario: Each trip needs to be approved by a logistics officer      
  Given Bindi has booked the following trip:
    | Destination | Date       | Nights | Hotel  | Flight |
    | New York    | 15-05-2020 | 3      | Hilton | FH-101 |
  And Logan is a logistics officer
  When Logan reviews his list of tasks
  Then Logan should see the following trips in his inbox:
    | Destination | Date            | Bookings      | Status           |
    | New York    | 15-18 May, 2020 | Flight, Hotel | Pending Approved |
 
Scenario: Travelers can see their approved upcoming trips        
  Given Bindi has booked the following trip:
    | Destination | Date       | Nights | Hotel  | Flight |
    | New York    | 15-05-2020 | 3      | Hilton | FH-101 |
  And the trip has been approved by logistics
  When she reviews her upcoming trips
  Then her upcoming trips should include:
    | Destination | Date            | Bookings      | Status   |
    | New York    | 15-18 May, 2020 | Flight, Hotel | Approved |

后勤官员 Logan 需要批准 Bindi 的旅行计划。

Logan, a logistics officer, needs to approve Bindi’s travel plans.

清单 7.5 中的每个场景都说明了商务旅行者在公司旅行系统中预订行程的部分故事。每个场景在语义上都建立在前一个场景的基础上;您可以按顺序阅读它们以了解全貌。但每个场景都是独立的,负责设置自己的测试数据。因此,如果一个场景失败,其他场景仍将正常工作。

Each scenario in listing 7.5 illustrates part of the story of a business traveler booking a trip in a corporate travel system. Each scenario semantically builds on the previous ones; you read them in sequence to get the full picture. But each scenario is independent and responsible for setting up its own test data. So, if one scenario fails, the others will still work correctly.

这种方法还有另一个更微妙的优势。这些场景中的每一个都是讨论可能发生的事情的起点。例如,在第三个场景中,Bindi 的旅行获得了批准。但如果旅行没有获得批准怎么办?这可能会导致另一个完整的功能文件,探讨更多的细节。

There is another more subtle advantage to this approach. Each of these scenarios is the starting point for a conversation about what else could happen. For example, in the third scenario, Bindi gets her trip approved. But what if the trip was not approved? This could lead to another complete feature file that explores the approval process in more detail.

7.7 但是所有的细节在哪里呢?

7.7 But where are all the details?

您会注意到,我们一直在研究的所有场景都非常注重业务,并且是高级场景。这是使它们比更细粒度的场景更易于维护的原因之一。不过,有时读者也希望更详细地了解用户需要做什么才能执行特定任务。在接下来的章节中,您将看到如何编写这种级别的高级测试,这些测试既非常易于维护,又能为任何需要它们的人提供所有细节性的低级细节。

You will notice how all the scenarios we have been looking at are very business focused and high level. This is one of the things that makes them easier to maintain than more granular scenarios. Sometimes, though, readers also like to know what a user needs to do in more detail to perform a particular task. In the following chapters you’ll see how to write high-level tests at this level that are both very maintainable and that still provide all the nitty-gritty low-level details for anyone who needs them.

概括

Summary

  • 您可以将示例转换为可执行规范,方法是使用“给定...何时...然后”格式的变体将示例写为场景。

  • You can turn examples into executable specifications by writing them as scenarios using variations on the Given ... When ... Then format.

  • 执行的场景被组织在特征文件中。

  • The executed scenarios are organized in feature files.

  • 您可以使用嵌入式数据表和表驱动场景使场景更加简洁、更具表现力。

  • You can make scenarios more concise and more expressive by using embedded data tables and table-driven scenarios.

  • 场景可以通过背景信息和标签来完成。

  • Scenarios can be completed with background information and tags.

希望您现在已经学到了足够的知识,能够编写自己的功能定义,并以简洁、富有表现力的场景进行说明。在下一章中,您将了解如何使用不同的语言和环境。

Hopefully you’ve now learned enough to be able to write your own feature definitions, illustrated with concise, expressive scenarios. In the next chapter, you’ll see how to automate and implement this process using different languages and environments.


1  您可以在 earning_points_from_flights.feature 文件中找到此功能,您可以在 src/test/resources/features/frequent_flyer/earning_points 文件夹中的示例项目中获得该文件。

1  You can find this feature in the earning_points_from_flights.feature file, which you can get in the sample project in the src/test/resources/features/frequent_flyer/earning_points folder.

2  请参阅丹·诺斯 (Dan North) 的文章“故事里有什么”,了解一些关于编写精彩故事和场景的有趣技巧:http://dannorth.net/whats-in-a-story/

2  See Dan North’s article, “What’s in a story,” for some interesting tips on writing well-pitched stories and scenarios: http://dannorth.net/whats-in-a-story/.

3  请参阅 Matt Wynne 和 Aslak Hellesøy 著《黄瓜书》(Pragmatic Bookshelf,2012 年)。

3  See Matt Wynne and Aslak Hellesøy, The Cucumber Book (Pragmatic Bookshelf, 2012).

4  为了规划和报告目的,使用标签或其他元数据将场景与故事联系起来并没有错。

4  There’s nothing wrong with using tags or some other metadata to relate a scenario to a story for planning and reporting purposes.

5  这些示例是用 Kotlin 编写的,Kotlin 是一种支持命令式和声明式编程风格的 JVM 语言。

5  These examples are written in Kotlin, a JVM language that supports both an imperative and a declarative style of programming.

6  设计和内容由 Xtensio ( https://xtensio.com )提供

6  Design and content courtesy of Xtensio (https://xtensio.com)

第 3 部分:如何构建它?用 BDD 方式编码

Part 3. How do I build it? Coding the BDD way

在第 2 部分的章节中,您了解了对话和协作在 BDD 流程中的重要性。您还了解到,您需要与团队成员合作,以清晰明确的格式定义验收标准,并且可以使用 Cucumber 等工具自动执行。

In the chapters in part 2, you learned about the importance of conversation and collaboration in the BDD process. You also learned that you need to work with team members to define acceptance criteria in a clear and unambiguous format that can be automated using tools such as Cucumber.

在第 3 部分中,我们将深入了解如何实现这种自动化。虽然整个团队都应该理解第 2 部分,但第 3 部分的技术性更强,测试人员和开发人员会对此更感兴趣。

In part 3, we look under the hood to see how you can do this automation. While part 2 should be understood by the whole team, part 3 gets a bit more technical and will be of more interest to testers and developers.

在第 8 章中,您将了解可用于自动化我们在第 2 部分中定义的可执行规范的工具。自动化是 BDD 的重要组成部分;它构成了验证所交付功能的自动验收标准的基础,以及说明和记录这些功能的动态文档的基础。

In chapter 8, you’ll learn about tools that you can use to automate the executable specifications we defined in part 2. Automation is an important part of BDD; it forms the basis of the automated acceptance criteria that verify the features being delivered and of the living documentation that illustrates and documents these features.

与任何代码库一样,如果编写得不好,自动验收测试的维护成本可能很高。在第 9 章中,您将学习如何构建和组织自动验收标准,使其更易于理解和维护。

As with any code base, the maintenance of automated acceptance tests can be costly if they’re not written well. In chapter 9, you’ll learn how to structure and organize your automated acceptance criteria to make them easier to understand and to maintain.

在第 10 章和第 11 章中,您将学习如何为 Web 应用程序编写自动验收标准,包括为什么以及何时应该将验收标准实现为 Web 测试。许多团队在这方面都遇到了困难,因此我们将详细介绍如何使用 Selenium WebDriver 编写高质量的自动化 Web 测试。在第 12 章中,您将了解 Screenplay 模式,这是一种强大的技术,可以使您的自动化代码更灵活、更具可扩展性。

In chapters 10 and 11, you’ll learn about writing automated acceptance criteria for web applications, including why and when you should implement your acceptance criteria as web tests. Many teams struggle in this area, so we’ll look at how to write high-quality automated web tests using Selenium WebDriver in some detail. And in chapter 12, you’ll learn about the Screenplay Pattern, a powerful technique to make your automation code more flexible and more scalable.

自动验收测试不仅仅适用于 Web 测试。在第 13 章中,我们将介绍针对纯业务规则和其他不需要通过 UI 验证的需求实施自动验收测试的技术。

Automated acceptance tests aren’t just for web testing. In chapter 13, we’ll look at techniques for implementing automated acceptance tests for pure business rules and other requirements that don’t need to be verified through the UI.

在第 14 章中,我们将重点介绍如何将自动化验收测试引入现有软件系统。您将了解 Journey Mapping 如何帮助您识别和可视化高价值工作流和测试场景,以及如何使用分层架构帮助您设计可扩展的测试自动化框架。为了帮助您更好地理解非 Java 测试自动化堆栈,我们将讨论用 TypeScript 和 Serenity/JS 编写的示例。

In chapter 14, we’ll focus on introducing automated acceptance tests to an existing software system. You’ll learn how Journey Mapping can help you identify and visualize high-value workflows and test scenarios, and how using a layered architecture can help you design scalable test automation frameworks. To help you gain a better understanding of non-Java test automation stacks, we’ll discuss examples written in TypeScript and using Serenity/JS.

在第 15 章中,我们将继续探讨测试自动化系统架构分层的想法,以及在代码中反映业务词汇的方法。我们还将研究 Serenity/JS API 如何帮助您使测试自动化代码可跨测试集成工具移植,并易于在项目和团队之间共享和重用。

In chapter 15, we’ll continue to explore the idea of layering the architecture of our test automation system and ways to reflect business vocabulary in our code. We’ll also look at how Serenity/JS APIs can help you make your test automation code portable across test integration tools and easy to share and reuse across projects and teams.

沟通和反馈是 BDD 流程中必不可少的部分,而动态文档是沟通流程中的一个重要部分。在第 16 章中,您将了解如何根据自动验收标准生成高质量的动态文档。我们将介绍功能就绪性和功能覆盖率等概念,您可以使用它们来跟踪项目进度和整体产品质量。我们还将看到编写良好的 BDD 单元测试如何充当有效的技术文档。

Communication and feedback are essential parts of the BDD process, and living documentation is an important part of the communication process. In chapter 16, you’ll see how to produce high-quality living documentation out of your automated acceptance criteria. We’ll introduce concepts such as feature readiness and feature coverage, which you use to keep tabs on project progress and overall product quality. We’ll also see how well-written BDD unit tests serve as effective technical documentation.

8 从可执行规范到自动验收测试

8 From executable specifications to automated acceptance tests

本章封面

This chapter covers

  • 自动化场景步骤的基本原则
  • The basic principles of automating your scenario steps
  • 步骤定义方法的职责
  • The responsibilities of a step definition method
  • 使用 Java 和 TypeScript 中的 Cucumber 实现步骤定义
  • Implementing step definitions using Cucumber in Java and TypeScript
  • 为测试设置虚拟环境
  • Setting up virtual environments for your tests

在前面几章中,您已经了解了 BDD 生命周期如何将验收标准转化为可执行规范。验收标准最初是写在故事卡背面的简短注释,有助于定义故事或功能何时完成。在 Three Amigos 会议或其他需求发现研讨会期间,这些注释被充实为更完整的示例和反例,以说明业务规则和目标。正如我们在上一章中看到的那样,您可以使用 Given ... When ... Then 符号以 Gherkin 场景的形式编写这些验收标准的更完整版本。1

Over the previous few chapters, you’ve seen how the BDD life cycle takes acceptance criteria and turns them into executable specifications. Acceptance criteria start as brief notes you write on the back of your story cards that help define when a story or feature is complete. During Three Amigos sessions or other requirements discovery workshops, these notes are fleshed out into more complete examples and counterexamples that illustrate business rules and objectives. And as we saw in the previous chapter, you can write more complete versions of these acceptance criteria in the form of Gherkin scenarios, using the Given ... When ... Then notation.1

BDD 中的很多价值都来自于关于这些场景的对话。这就是为什么协作编写这些场景如此重要的原因。并非所有场景都需要自动化;有些场景可能太棘手,无法以经济高效的方式实现自动化,可以留给手动测试。其他场景可能只对业务有边际利益,最好作为单元测试或集成测试来实现。还有一些场景可能是实验性的,可能还不够好理解,无法定义清晰的场景;在这种情况下,进行一些初始原型设计以更好地了解真正需要什么可能是值得的。2

A lot of the value in BDD comes from the conversations about these scenarios. This is why collaborating to write these scenarios is so important. Not all scenarios need to be automated; some may be too tricky to automate cost-effectively and can be left to manual testing. Others may only be of marginal interest to the business and might be better off implemented as unit or integration tests. Still others may be experimental and might not be understood well enough to define clear scenarios; in this case, it could be worthwhile to do some initial prototyping to get a better feel for what’s really needed.2

但是,当一个场景可以自动化,当自动化是有意义的,并且当自动化做得很好时,场景自动化会带来一系列不可否认的好处:

But when a scenario can be automated, when it makes sense to do so, and when it’s done well, automating the scenario brings its own set of undeniable benefits:

  • 测试人员花在重复回归测试上的时间更少。当验收标准和相应的场景是与测试人员密切合作编写的,这些场景的自动化版本会让测试人员对新版本更有信心。测试人员可以更容易地理解和联系自动化测试正在验证的内容,因为他们参与了这些测试的定义。此外,测试人员收到的用于测试的应用程序已经通过了一系列更简单的测试用例,让测试人员可以专注于更复杂或探索性的测试。

  • Testers spend less time on repetitive regression testing. When acceptance criteria and the corresponding scenarios are written in close collaboration with testers, automated versions of these scenarios give testers more confidence in new releases. The testers can understand and relate more easily to what the automated tests are verifying, because they took part in defining them. In addition, the application that the testers receive for testing will have already passed a broad range of simpler test cases, letting the testers focus on more complex or exploratory testing.

  • 新版本可以更快、更可靠地发布。由于需要的手动测试更少,因此可以更有效地推出新版本。新版本不太可能引入回归。如果您尝试实现持续集成、持续交付或持续部署,全面的自动化测试至关重要。

  • New versions can be released faster and more reliably. Because less manual testing is required, new releases can be pushed out more efficiently. New versions are less likely to introduce regressions. Comprehensive automated testing is essential if you’re trying to implement continuous integration, continuous delivery, or continuous deployment.

  • 自动化场景可以更准确地展示项目的当前状态。您可以使用它们构建进度仪表板,根据自动化场景的结果描述已交付的功能及其测试方式。

  • The automated scenarios give a more accurate vision of the current state of the project. You can use them to build a progress dashboard that describes which features have been delivered and how they’ve been tested, based on the results of the automated scenarios.

持续集成、持续交付和持续部署

Continuous integration, continuous delivery, and continuous deployment

持续集成是一种实践,每当将新的代码更改提交到源代码存储库时,都会自动构建和测试项目。持续集成是一种有价值的反馈机制,可以尽早提醒开发人员潜在的集成问题或回归。但要真正有效,持续集成很大程度上依赖于一套强大而全面的自动化测试。

Continuous integration is a practice that involves automatically building and testing a project whenever a new code change is committed to the source code repository. Continuous integration is a valuable feedback mechanism, alerting developers to potential integration issues or regressions as early as possible. But to be really effective, continuous integration strongly relies on a robust and comprehensive set of automated tests.

持续交付是持续集成的扩展,其中每个构建都是一个潜在的版本。每当开发人员将新代码放入源代码存储库时,构建服务器就会编译一个新的候选版本。如果此候选版本通过了一系列自动质量检查(单元测试、自动验收测试、自动性能测试、代码质量指标等),则可以在业务利益相关者批准后立即投入生产。

Continuous delivery is an extension of continuous integration, where every build is a potential release. Whenever a developer puts new code into the source code repository, a build server compiles a new release candidate version. If this release candidate passes a series of automated quality checks (unit tests, automated acceptance tests, automated performance tests, code quality metrics, etc.), it can be pushed into production as soon as business stakeholders give their go-ahead.

持续部署与持续交付类似,但没有手动审批阶段。任何通过自动质量检查的候选版本都将自动部署到生产中。部署过程本身通常使用 Chef、Puppet 或 Octopus Deploy 等工具实现自动化。

Continuous deployment is similar to continuous delivery, but there’s no manual approval stage. Any release candidate that passes the automated quality checks will automatically be deployed into production. The deployment process itself is often automated using tools like Chef, Puppet, or Octopus Deploy.

持续交付和持续部署都鼓励更加精简、高效的部署流程。两者都需要对应用程序的自动化测试套件有很高的信心。

Both continuous delivery and continuous deployment encourage a much more streamlined, efficient deployment process. And both require a very high degree of confidence in the application’s automated test suites.

在第 6 章中,您了解了如何使用 Given ... When ... Then 符号来表达场景。在本章中,您将学习如何编写自动化这些场景的测试代码(见图 8.1)。

In chapter 6 you saw how to express scenarios using the Given ... When ... Then notation. In this chapter, you’ll learn how to write the test code that automates these scenarios (see figure 8.1).

图 8.1 在本章中,我们将研究如何将可执行规范转变为自动验收测试。

Figure 8.1 In this chapter we’ll look at how to turn executable specifications into automated acceptance tests.

当你自动化这些场景时,它们就变成了可执行的规范,但这些测试将被报告为待定或未完成,直到您编写实际运行应用程序并验证结果的底层代码。一旦发生这种情况,您就可以谈论自动验收测试

When you automate these scenarios, they become executable specifications, but these will be reported as pending, or incomplete, until you write the underlying code that actually exercises the application and verifies the outcomes. Once this happens, you can talk of automated acceptance tests.

在第 3 章中,您了解了如何使用 Java 和 Maven 设置一个简单的 Cucumber 项目,如何在功能文件中编写功能和场景,以及如何为这些场景实现基本的自动化代码(“粘合代码”)。在本章中,我们将更详细地介绍 Cucumber 等工具如何让我们将可执行规范转换为自动验收测试。但在这之前,我们需要讨论一些通用原则,无论您选择哪种工具,这些原则都适用。

In chapter 3, you saw how to set up a simple Cucumber project using Java and Maven, how to write features and scenarios in feature files, and how to implement basic automation code (“glue code”) for these scenarios. In this chapter, we will look at how tools like Cucumber allow us to convert executable specifications into automated acceptance tests in much more detail. But before we do this, we need to discuss some general principles that will apply no matter what tool you choose.

8.1 自动化场景介绍

8.1 Introduction to automating scenarios

我们先来看看具体的工具,然后再来了解一下基础知识。在第 7 章中,您了解了如何用纯文本场景来描述需求,如下所示:

Before we look at specific tools, let’s go through the basics. In chapter 7, you saw how to describe requirements in terms of plain-text scenarios like the following:

场景:欧洲境外的航班根据飞行距离赚取积分    
  假设伦敦到纽约的距离是 5500 公里                    
  Tara是一名常旅客                                    
  她完成从伦敦到纽约的飞行后                  
  那么她应该获得 550 分                                          
Scenario: Flights outside Europe earn points based on distance traveled    
  Given the distance from London to New York is 5500 km                    
  And Tara is a Frequent Flyer traveler                                    
  When she completes a flight between London and New York                  
  Then she should earn 550 points                                          

场景标题

The scenario title

场景步骤

The scenario steps

此场景只是结构松散的文本。它解释了您要说明的要求以及您打算如何证明您的应用程序满足此要求。但您实际执行每个步骤的方式将取决于您的应用程序以及您决定如何与其交互。

This scenario is just loosely structured text. It explains what requirement you’re trying to illustrate and how you intend to demonstrate that your application fulfills this requirement. But how you actually perform each step will depend on your application and on how you decide to interact with it.

例如,上述场景由四个步骤组成。每个步骤都需要与应用程序交互以准备测试环境、执行测试操作并检查结果。例如,考虑Given第一步

For example, the preceding scenario is made up of four steps. Each of these steps needs to interact with the application to prepare the test environment, perform the action under test, and check the results. For example, consider the first Given step:

假设伦敦到纽约的距离是 5500 公里
Given the distance from London to New York is 5500 km

这里您需要配置一个测试数据库,以提供伦敦和纽约之间的正确距离。您可以通过多种方式执行此操作:将数据直接注入测试数据库、调用 Web 服务或操作用户界面。文本描述了您打算做什么,但具体如何做将取决于应用程序的性质和您的技术选择。

Here you need to configure a test database to provide the correct distance between London and New York. You could do this in many ways: inject data directly into a test database, call a web service, or manipulate a user interface. The text describes what you intend to do, but how you do this will depend on the nature of the application and on your technical choices.

其他步骤类似,例如When第一步描述您正在测试的操作:

The other steps are similar. For example, the first When step describes the action you’re testing:

她完成伦敦和纽约之间的飞行后
When she completes a flight between London and New York 

再次强调,这一步描述了您要做的事情。您想记录从伦敦飞往纽约的航班,以便查看 Tara 赚了多少积分。但如何做到这一点需要更多有关您的应用程序及其架构的知识。

Again, this step describes what you want to do. You want to record a flight from London to New York so that you can check how many points Tara earns. But how you do this requires more knowledge about your application and its architecture.

像 Cucumber 这样的工具无法自行将文本场景转变为自动化测试;它们需要您的帮助。您需要一种方法来告诉您的测试框架这些步骤对您的应用程序意味着什么,以及它必须如何操作或查询您的应用程序才能执行其任务。这就是步骤定义发挥作用的地方。

Tools like Cucumber can’t turn a text scenario into an automated test by themselves; they need your help. You need a way to tell your testing framework what each of these steps means in terms of your application and how it must manipulate or query your application to perform its task. This is where step definitions come into play.

8.1.1 步骤定义解释步骤

8.1.1 Step definitions interpret the steps

定义本质上是一些代码,它解释特征文件中的文本并知道每个步骤要做什么(见图 8.2)。

Step definitions are essentially bits of code that interpret the text in feature files and know what to do for each step (see figure 8.2).

图 8.2 场景的每一行(或步骤)映射到步骤定义测试。

Figure 8.2 Each line (or step) of a scenario maps to a step definition test.

根据所使用的测试自动化库,步骤定义可以用多种编程语言实现。在某些情况下,用于实现步骤定义的语言甚至可能与用于编写应用程序的语言不同。

Step definitions can be implemented in a variety of programming languages depending on the test automation library being used. In some cases, the language used to implement the step definitions may even be different than that used to write the application.

例如,用 Java 编写的 Cucumber 步骤定义可能如下所示:

For example, a Cucumber step definition written in Java might look like this:

航班数据库 flightDatabase = FlightDatabase.instance();
 
@Given("从 {} 到 {} 的距离为 {int} 公里")         。❶
公共无效记录飞行距离(字符串出发,        
                                 字符串目的地,      
                                 int 距离(公里) {      
    flightDatabase.recordTripDistance()                   
                  .from(出发)                        
                  .to(目的地)                        
                  .as(distanceInKm).kilometres();         
}
FlightDatabase flightDatabase = FlightDatabase.instance();
 
@Given("the distance from {} to {} is {int} km").        
public void recordFlightDistance(String departure,       
                                 String destination,     
                                 int distanceInKm) {     
    flightDatabase.recordTripDistance()                  
                  .from(departure)                       
                  .to(destination)                       
                  .as(distanceInKm).kilometres();        
}

哪些文本应该触发此步骤定义

What text should trigger this step definition

步骤定义方法

The step definition method

实现此步骤的代码

The code that implements this step

不同语言的形式有所不同,但基本信息是相同的。例如,在 TypeScript 中,可以这样写同样的例子:

The form varies from one language to another, but the essential information is the same. In TypeScript, for example, the same example could be written like this:

const flightDatabase = FlightDatabase.instance();
 
给定('从 {} 到 {} 的距离为 {int} 公里',
      函数(原点:字符串,目的地:字符串,距离(公里):数字){
            航班数据库.记录行程距离()
                          .from(出发)
                          .to(目的地)
                          .as(距离(公里)).公里()
})
const flightDatabase = FlightDatabase.instance();
 
Given('the distance from {} to {} is {int} km',
      function (origin: string, destination: string, distanceInKm: number) {
            flightDatabase.recordTripDistance()
                          .from(departure)
                          .to(destination)
                          .as(distanceInKm).kilometres()
})

如果您使用 .NET 堆栈,使用 C#,则可以使用 SpecFlow 编写一些非常相似的代码:

If you are working with a .NET stack, using C#, you could write some very similar code using SpecFlow:

航班数据库 flightDatabase = FlightDatabase.instance();
 
[Given(@"(.*) 和 (.*) 之间的飞行距离为 (.*) 公里")]
公共无效DefineTheFlyingDistanceForATrip(字符串出发,
                                            字符串目标,
                                            int 距离)
{
    航班数据库.记录行程距离()
                  .from(出发)
                  .to(目的地)
                  .as(距离(公里)).公里();
}
FlightDatabase flightDatabase = FlightDatabase.instance();
 
[Given(@"the flying distance between (.*) and (.*) is (.*) km")]
public void DefineTheFlyingDistanceForATrip(string departure,
                                            string destination,
                                            int distance)
{
    flightDatabase.recordTripDistance()
                  .from(departure)
                  .to(destination)
                  .as(distanceInKm).kilometres();
}

无论您使用哪种语言或工具,测试自动化库都会读取功能文件并确定每个步骤应调用的方法。您还可以告诉库如何从文本中提取重要数据并将该数据传递给步骤定义方法。但步骤定义的工作是执行此步骤所需的一切。

No matter what language or tool you are using, the test automation library will read the feature files and figure out what method it should call for each step. You can also tell the library how to extract important data out of the text and pass that data to the step definition method. But it’s the step definition’s job to do whatever needs to be done to perform this step.

由于功能文件和注释由自由文本组成,因此始终存在这样的风险:当功能文件中的文本被修改时,您可能会忘记更新注释中的文本,反之亦然。在这种情况下,受影响的场景将再次被标记为待处理。为了帮助开发人员管理此类问题,许多现代 IDE 都提供了 BDD 工具插件,例如 Cucumber 和 SpecFlow,这些插件允许您从场景导航到步骤定义代码,并突出显示没有匹配方法的场景中的步骤。

Because the feature files and annotations consist of free text, there’s always a risk that when the text in a feature file is modified, you may forget to update the text in the annotation, or vice versa. In this case, the affected scenario or scenarios will be flagged as pending once again. To help developers manage this sort of issue, many modern IDEs have plug-ins for BDD tools such as Cucumber and SpecFlow, which allow you to navigate from scenarios to step definition code, and that highlight steps in scenarios that don’t have matching methods.

在本章的剩余部分中,我们将更详细地介绍如何编写步骤定义方法,特别是 Java 和 TypeScript。但在开始之前,让我们先看看如何在这两种语言中建立一个专业的测试自动化项目这些语言。

In the remainder of the chapter, we will look at how to write your step definition methods in more detail, focusing particularly on Java and TypeScript. But before we start, let’s see how to set up a professional test automation project in both these languages.

8.2 设置你的项目

8.2 Setting up your project

那里您可以使用许多 BDD 工具来自动化我们一直在研究的场景,这些工具使用不同的语言并通常针对不同的环境。选择哪种工具适合您的团队将取决于团队对特定语言和环境的熟悉程度(以及他们学习新语言和环境的意愿!)、您的项目使用的技术堆栈以及您的 BDD 活动的目标和目标受众。

There are many BDD tools you can use to automate scenarios like the ones we’ve been looking at, using different languages and often targeting different environments. The choice of which tool is right for your team will depend on how comfortable the team is with a particular language and environment (and how willing they are to learn a new one!), what technology stack your project is using, and the goals and target audience of your BDD activities.

在本章的其余部分中,我们将学习如何使用 Java 和 JavaScript 中的示例编写高质量的步骤定义代码,这是撰写本文时最流行的选择。在本章的最后,我们将简要概述 .NET 和 Python 的 BDD 工具。我们将研究的工具列表远非详尽无遗,但无论您选择哪种工具,我们将讨论的技术都应该普遍适用。

Throughout the rest of this chapter, we will learn how to write high-quality step definition code using examples in Java and JavaScript,3 the most popular options at the time of writing. At the end of the chapter, we will give a brief overview of BDD tools for .NET and Python. The list of tools we’ll study is far from exhaustive, but the techniques we’ll discuss should be generally applicable no matter which tool you choose.

自动化测试和生产代码一样,都是代码。如果我们把它们看作简单的脚本,那些不需要太多技巧或心思就能轻松快速编写的东西,我们就会陷入麻烦。如果自动化验收测试套件设计不当,它们会增加维护开销,在添加新功能时更新和修复的成本会高于它们对项目的价值贡献。因此,设计好验收测试非常重要。在本章中,我们还将介绍一些技术和模式,它们可以帮助您编写有意义、可靠且可维护的自动化验收测试。

Automation tests are code, just like production code. It’s when we think of them as simple scripts, things that can be easily and quickly written with little skill or care involved, that we get into trouble. When automated acceptance test suites are poorly designed, they can add to the maintenance overhead, costing more to update and fix when new features are added than they contribute in value to the project. For this reason, it’s important to design your acceptance tests well. In this chapter, we’ll also look at a number of techniques and patterns that can help you write automated acceptance tests that are meaningful, reliable, and maintainable.

为了探索每个工具的功能,我们将研究如何自动化涉及前几章介绍的 Flying High 航空公司的常旅客应用程序的场景。常旅客计划旨在鼓励旅客乘坐 Flying High 航班,让他们积累可用于航班或其他购买的积分。在本章的示例中,我们将研究积累和管理常旅客积分的一些要求。但在进入代码之前,让我们看看如何从头开始设置测试自动化项目并开始编写自己的测试代码。

To explore the features of each tool, we’ll look at how you might automate scenarios involving the Flying High airline’s Frequent Flyer application, introduced in the previous chapters. The Frequent Flyer program is designed to encourage flyers to fly with Flying High by allowing them to accumulate points that they can spend on flights or on other purchases. In this chapter’s examples, we’ll study some of the requirements around accumulating and managing Frequent Flyer points. But before we get into the code, let’s see how you can set up a test automation project from scratch and start writing your own test code.

8.2.1 使用 Java 或 TypeScript 设置 Cucumber 项目

8.2.1 Setting up a Cucumber project in Java or TypeScript

在第 3 章中,我们了解了如何使用 GitHub 上的 Serenity Cucumber Starter 项目 ( http://mng.bz/gRKV )创建基本的项目结构;您可以自行克隆此项目,也可以将其用作模板在 GitHub 上创建自己的项目(见图 8.3)。Serenity Cucumber Starter 项目还包含一些示例配置文件和一个简单的 Web 测试,可帮助您入门。如果您更喜欢 TypeScript,Serenity/JS 项目还有许多模板项目可供您使用http://mng.bz/aPp7http://mng.bz/yapB)。

In chapter 3, we saw how you can create a basic project structure using the Serenity Cucumber Starter project on GitHub (http://mng.bz/gRKV); you can clone this project yourself or use it as a template to create your own project on GitHub (see figure 8.3). The Serenity Cucumber Starter project also contains some sample configuration files and a simple web test to get you started. The Serenity/JS project also has a number of template projects you can use if you prefer TypeScript (http://mng.bz/aPp7 and http://mng.bz/yapB).

图 8.3 您还可以使用 GitHub 上的 Serenity Cucumber Starter 模板创建一个启动项目。

Figure 8.3 You can also create a starter project using the Serenity Cucumber Starter template on GitHub.

8.2.2 在 Java 中组织 Cucumber 项目

8.2.2 Organizing a Cucumber project in Java

在图 8.4 中,您可以看到用 Java 编写并使用 Maven 或 Gradle 作为构建工具的 Cucumber 测试套件的相当典型的项目组织。

In figure 8.4, you can see a fairly typical project organization for a Cucumber test suite written in Java and using Maven or Gradle as a build tool.

图 8.4 使用 Java 的 Cucumber 项目示例

Figure 8.4 An example of a Cucumber project using Java

该项目遵循 Maven 约定,该约定在 Java 世界中被广泛使用并有助于使新加入该项目的开发人员更容易阅读项目。

This project follows the Maven conventions, which are widely used in the Java world and help to make a project easier to read for developers new to the project.

应用程序代码和资源位于 src/main 文件夹 (1) 中。这就是我们将如何组织示例项目,但并非所有项目都是这样。许多较大的 Maven 和 Gradle 项目通常有多个模块,验收测试套件是众多模块中的一个。在这样的项目中,可能不需要 src/main/java 文件夹中的任何应用程序代码。

Application code and resources go in the src/main folder (1). This is how we will organize our sample project, but not all projects are like this. Many larger Maven and Gradle projects often have multiple modules, with the acceptance test suite being one module among many others. In projects like this, there may be no need for any application code in the src/main/java folder.

任何与自动化测试相关的内容,包括可执行规范,都放在 src/test 文件夹 (2) 中。在此文件夹中,您将找到一个 java 子文件夹,其中存放 Java 测试代码,还有另一个名为 resources 的文件夹。

Anything related to automated tests, including executable specifications, goes in the src/test folder (2). In this folder you will find a java subfolder, where your Java test code goes, and another folder called resources.

java 文件夹包含几个重要的类和子文件夹,它们可以以多种不同的方式组织。此处显示的只是一个示例。在此处显示的项目中,Cucumber 功能文件由名为的测试运行器类执行AcceptanceTestSuite(5)实际执行 Cucumber 场景中步骤的代码称为粘合代码,可以在stepdefinitions包中找到(4). 其他测试自动化框架代码放在包中,比如这里显示的域包(3)。

The java folder contains several important classes and subfolders, and they can be organized in many different ways. The one shown here is just one example. In the project shown here, the Cucumber feature files are executed by a test runner class called AcceptanceTestSuite (5). The code that actually executes the steps in the Cucumber scenarios is called glue code and can be found in the stepdefinitions package (4). Other test automation framework code goes in packages, such as the domain package shown here (3).

resources 文件夹包含运行测试所需的非 Java 文件,特别是 features 目录 (6),其中包含 Cucumber 功能文件。一个常见的惯例是将功能文件放在 src/test/resources/features 文件夹中,这也是本书中我们将遵循的惯例。但是,如果同一目录中的功能文件太多,它们会变得难以阅读,因此功能文件通常按功能或其他方式分组结构。

The resources folder contains non-Java files needed to run your tests, in particular the features directory (6), which will contain your Cucumber feature files. A common convention is to place your feature files in the src/test/resources/features folder, and this is the convention we will follow throughout this book. If there are too many feature files in the same directory, however, they become hard to read, so feature files are generally grouped by capability or in some other structure.

8.2.3 使用 TypeScript 组织 Cucumber 项目

8.2.3 Organizing a Cucumber project in TypeScript

图 8.5,您可以看到一个用 TypeScript 实现的 Node.js 项目的典型布局,该项目在功能目录的结构中使用了流行的 Cucumber JS 约定。在我们的示例项目中,应用程序代码和资源位于 src 文件夹中 (1)。但是,Node.js 项目的结构和布局并不像 Java 世界中那样同质。您会发现,不同的项目遵循不同的约定,具体取决于其作者的偏好以及所使用的工具和框架。

In figure 8.5, you can see a fairly typical layout of a Node.js project implemented in TypeScript and using popular Cucumber JS conventions around the structure of the features directory. In our example project, application code and resources go in the src folder (1). However, the structure and layout of Node.js projects is not as homogenous as in the Java world. You will find that different projects follow different conventions depending on their authors’ preferences, as well as tools and frameworks used.

图 8.5 使用 TypeScript 的 Cucumber 项目示例

Figure 8.5 An example of a Cucumber project using TypeScript

在测试自动化方面,使用 Cucumber JS 的项目倾向于遵循 Cucumber 惯例,将功能文件存储在 features 文件夹 (2) 下。此文件夹通常包含子文件夹,例如 step_definitions,其中包含包含步骤定义库的文件,或 support,其中包含任何支持代码,如配置、参数类型定义等。值得注意的是,Cucumber JS 通常配置为在 features 文件夹下的所有 TypeScript 或 JavaScript 文件中查找步骤定义。但是,尽管使用这些子文件夹只是一种惯例,即使您以不同的方式命名它们,您的代码也会正常工作,但许多 IDE 都希望遵循这些惯例来为您提供代码导航支持。

When it comes to test automation, projects using Cucumber JS tend to follow Cucumber conventions and store feature files under the features folder (2). This folder typically contains subfolders such as step_definitions that host files containing step definition libraries, or support, which contains any supporting code such as configuration, parameter types definitions, and so on. It’s worth noting that Cucumber JS is typically configured to look for step definitions in all the TypeScript or JavaScript files under the features folder. However, even though using those subfolders is just a convention and your code will work even if you named them differently, many IDEs expect those conventions to be followed to provide you with code navigation support.

8.3 运行 Cucumber 场景

8.3 Running Cucumber scenarios

一个如果我们实际上无法运行任何测试,那么测试自动化项目就没什么用。在本节中,您将学习如何使用 Java 或 JavaScript 设置和配置测试运行器来执行您的 Cucumber 场景。

A test automation project isn’t much use if we can’t actually run any tests. In this section, you will learn how to set up and configure test runners in Java or JavaScript that will execute your Cucumber scenarios.

8.3.1 Java 中的 Cucumber 测试运行器类

8.3.1 Cucumber test runner classes in Java

Java 项目,您需要编写一个特殊的类来运行您的场景。此类不包含此类测试代码,仅包含配置逻辑。以下清单中展示了一个非常简单的 Cucumber 运行器类。

In a Java project, you need to write a special class to run your scenarios. This class contains no such test code, just configuration logic. A very simple Cucumber runner class is illustrated in the following listing.

清单 8.1 一个简单的 Cucumber 运行器类

Listing 8.1 A simple Cucumber runner class

包 com.manning.bddinaction.frequentflyer;
 
导入 net.serenitybdd.cucumber.CucumberWithSerenity;
导入 org.junit.runner.RunWith;
 
@RunWith(Cucumber.class)              
公共类 RunCucumberTest {}       
package com.manning.bddinaction.frequentflyer;
 
import net.serenitybdd.cucumber.CucumberWithSerenity;
import org.junit.runner.RunWith;
 
@RunWith(Cucumber.class)             
public class RunCucumberTest {}      

@RunWith 注释

The @RunWith annotation

该类本身应该是空的。

The class itself should be empty.

此类将在包及其子目录中查找功能文件com.manning.bddinaction.frequentflyer。然而,在实践中,这可能会有点混乱,使功能文件更难找到。

This class will look for feature files inside the com.manning.bddinaction.frequentflyer package and its subdirectories. However, in practice this can be a little confusing and make the feature files harder to find.

更好的方法是使用@CucumberOptions注释配置更精细的细节,如下面的清单所示。

A better approach is to use the @CucumberOptions annotation to configure the finer details, as in the following listing.

清单 8.2@CucumberOptions配置测试运行器类的部分

Listing 8.2 The @CucumberOptions section to configure your test runner class

包 com.manning.bddinaction.frequentflyer;
 
导入io.cucumber.junit.CucumberOptions;
导入 net.serenitybdd.cucumber.CucumberWithSerenity;
导入 org.junit.runner.RunWith;
 
@RunWith(CucumberWithSerenity.class)
@CucumberOptions(
    features = "classpath:features",                                    
    标签 = “@important”,                                                
    胶水 = “com.manning.bddinaction.frequentflyer.stepdefinitions”     
    插件 = {“pretty”,“json:target/cucumber.json”}                     
公共类AcceptanceTestSuite {}
package com.manning.bddinaction.frequentflyer;
 
import io.cucumber.junit.CucumberOptions;
import net.serenitybdd.cucumber.CucumberWithSerenity;
import org.junit.runner.RunWith;
 
@RunWith(CucumberWithSerenity.class)
@CucumberOptions(
    features = "classpath:features",                                   
    tags = "@important",                                               
    glue = "com.manning.bddinaction.frequentflyer.stepdefinitions",    
    plugin = {"pretty","json:target/cucumber.json"}                    
)
public class AcceptanceTestSuite {}

指定功能文件的根目录

Specifies the root directory of your feature files

仅运行带有此标签的场景

Only runs scenarios with this tag

胶水代码位于此包下。

Glue code goes under this package.

可以使用 Cucumber 插件定义额外的功能。

Extra functionality can be defined using Cucumber plug-ins.

从清单 8.2 的例子中可以看出,@CucumberOptions注释为您提供了广泛的选项。其中最重要的是 features 属性,它告诉 Cucumber 在哪里找到您的功能文件,以及 glue 属性,它告诉 Cucumber 在哪里查找您的粘合代码(我们将在稍后详细讨论)。其他一些属性包括 tags 属性,它允许您定义标签或 Cucumber 标签表达式(稍后会详细介绍)以将测试执行限制为测试的子集,以及 plug-in 属性,它允许您添加额外的功能,例如在执行测试时将场景写入控制台,以及将 Cucumber 测试结果生成为 JSON 文件(这里显示的两个插件就是做这个的)。

As you can see from the example in listing 8.2, the @CucumberOptions annotation gives you a wide range of options. The most important of these is the features attribute, which tells Cucumber where to find your feature files, and the glue attribute, which tells Cucumber where to look for your glue code (which we will talk more about shortly). Some of the other attributes include the tags attribute, which lets you define a tag or Cucumber tag expression (more on these later) to limit test execution to a subset of your tests, and the plug-in attribute, which lets you add extra capabilities such as writing the scenarios to the console as the tests are executed, and generating the Cucumber test results as a JSON file (which is what the two plug-ins shown here do).

8.3.2 在 JavaScript 和 TypeScript 中运行 Cucumber 场景

8.3.2 Running Cucumber scenarios in JavaScript and TypeScript

项目使用 Cucumber JS 通过其命令行界面调用该工具,并且不需要运行器类,因为所有配置参数都作为命令行上的参数提供:

Projects using Cucumber JS invoke the tool using its command-line interface and don’t require a runner class, as all the configuration parameters are provided as arguments on the command line:

npx cucumber-js \                            
    --require-module ts-node/register \      
    --require'features/**/*.ts' \           
    --tags '@important 而非 @wip' \       
    ‘features/**/*.feature’                  
npx cucumber-js \                           
    --require-module ts-node/register \     
    --require 'features/**/*.ts' \          
    --tags '@important and not @wip' \      
    'features/**/*.feature'                 

调用 cucumber-js 命令行界面

Invokes cucumber-js command line interface

注册对执行 TypeScript 代码的支持,无需先将其转换为 JavaScript

Registers support for executing TypeScript code without having to transpile it to JavaScript first

扫描任何 TypeScript (.ts) 文件以查找步骤定义和 Cucumber 钩子

Scans any TypeScript (.ts) files for step definitions and Cucumber hooks

使用 Cucumber 标签表达式来识别要运行的场景

Uses Cucumber Tag Expression to identify what scenarios to run

告诉 Cucumber 运行哪些功能文件

Tells Cucumber what feature files to run

当然,每次运行测试套件时都必须指定所有这些参数,这相当不方便。更好的方法是将它们捕获到项目根目录中 cucumber.js 文件中定义的 Cucumber 配置文件中:

Of course, having to specify all these arguments every time we want to run our test suite would be rather inconvenient. A better way to do that is to capture them in a Cucumber profile defined in the cucumber.js file in the project root directory:

//黄瓜.js
模块.导出 = {
    默认值:`--require-module ts-node/register --require 'features/**/*.ts'`,
}
// cucumber.js
module.exports = {
    default: `--require-module ts-node/register --require 'features/**/*.ts'`,
}

Cucumber JS 会自动检测此文件,这使我们能够简化之前使用的命令:

Cucumber JS detects this file automatically, which allows us to simplify the command we used previously:

npx cucumber-js'features/**/*.feature'
npx cucumber-js 'features/**/*.feature'

不过,我们可以更进一步,将 Cucumber 测试套件的执行连接到我们在 package.json 中定义的 NPM 测试脚本:

We can take this a step further, though, and wire execution of Cucumber test suite to an NPM test script defined in our package.json:

“脚本”:{
  “测试”:“cucumber-js'features/**/*.feature'”
},
"scripts": {
  "test": "cucumber-js 'features/**/*.feature'"
},

有了这个脚本定义,可以通过运行来调用测试套件

With this script definition in place, the test suite can be invoked by running

npm 测试
npm test

此配置还允许我们执行与给定名称匹配的场景,

This configuration allows us to also execute scenarios matching a given name,

npm test -- --name='欧洲以外航班'
npm test -- --name='Flights outside Europe'

或者匹配给定的 Cucumber 标签表达式:

or matching a given Cucumber tag expression:

npm 测试 -- --tags='@important 而不是 @wip'
npm test -- --tags='@important and not @wip'

请注意,这两个命令使用双破折号 ( ) 将命令与 Cucumber 参数(例如或)--分隔开。双破折号是一个特殊的 NPM 参数,指示它将后面的任何参数传递给 NPM 测试执行的脚本。要了解有关可用npm test--name--tags选项, 跑步

Note that these two commands use a double dash (--) to delimit the npm test command from Cucumber arguments such as --name or --tags. The double dash is a special NPM argument that instructs it to pass any arguments that follow to the script executed by the NPM test. To find out more about the available options, run

npx cucumber-js——帮助
npx cucumber-js --help

8.4 编写粘合代码

8.4 Writing glue code

工具例如 Cucumber 的工作原理是读取 Gherkin 场景并为场景中的每一行运行相应的代码。Cucumber 的工作是确定它需要为场景中任何给定的行调用什么方法,以及需要将哪些测试数据传递给测试自动化代码。让我们从以下场景开始:

Tools such as Cucumber work by reading Gherkin scenarios and running a corresponding piece of code for each line in the scenario. Cucumber’s job is to figure out what method it needs to call for any given line in a scenario and what test data it needs to pass to the test automation code. Let’s start with the following scenario:

场景:欧洲境内航班可赚取 100 积分
  鉴于Tara 是标准飞行常客计划会员
  她乘坐经济舱从巴黎和柏林起飞
时  那么她应该获得 100 分
Scenario: Flights within Europe earn 100 points
  Given Tara is a Standard Frequent Flyer member
  When she flies from Paris and Berlin in Economy class
  Then she should earn 100 points

我们可以通过编写一些粘合代码将此场景转变为自动化测试,这些粘合代码将场景的自然语言与实际的自动化代码绑定在一起;这就是我们告诉 Cucumber 在场景的每个步骤中需要执行哪些代码的方式。我们称之为步骤定义方法。所示场景中第一行的步骤定义方法可能如下所示。

We can turn this scenario into an automated test by writing some glue code, which binds the natural language of the scenarios to the actual automation code; it is how we tell Cucumber what code it needs to execute at each step of a scenario. We call this a step definition method. The step definition method for the first line in the scenario shown might go something like the following.

清单 8.3 Java 中的简单步骤定义方法

Listing 8.3 A simple step definition method in Java

FrequentFlyerMember 会员;                                                 
 
@Given(“Tara 是标准飞行常客会员”)    
公共无效aStandardFrequentFlyerMember(){
会员 = FrequentFlyerMember.named(“Tara”)                                 
                              .withStatus(FrequentFlyerStatus.Standard);    
}
FrequentFlyerMember member;                                                
 
@Given("Tara is a Standard Frequent Flyer member")    
public void aStandardFrequentFlyerMember() {
 member = FrequentFlyerMember.named("Tara")                                
                              .withStatus(FrequentFlyerStatus.Standard);   
}

我们将常旅客会员记录在一个变量中,以便在以下步骤中使用。

We record the frequent flyer member in a variable to be used in the following steps.

我们使用一个简单的构建器方法创建一个新的飞行常客会员。

We create a new Frequent Flyer member using a simple builder method.

或者在 TypeScript 中,相同的步骤定义可能如下所示。

Or in TypeScript, the same step definition could look like the following.

清单 8.4 TypeScript 中的简单步骤定义方法

Listing 8.4 A simple step definition method in TypeScript

让成员:FrequentFlyerMember;                  
 
Given('Tara 是标准飞行常客会员', function() {
    会员 = 新 FrequentFlyerMember({            
        名字:'Tara',
        状态:FrequentFlyerStatus.Standard,
    });
});
let member: FrequentFlyerMember;                 
 
Given('Tara is a Standard Frequent Flyer member', function() {
    member = new FrequentFlyerMember({           
        name: 'Tara',
        status: FrequentFlyerStatus.Standard,
    });
});

我们将常旅客会员记录在一个变量中,以便在以下步骤中使用。

We record the frequent flyer member in a variable to use in the following steps.

我们通过向域对象构造函数注入对象文字来创建新的常旅客成员。

We create a new Frequent Flyer member by injecting an object literal into a domain object constructor.

8.4.1 使用步骤定义参数注入数据

8.4.1 Injecting data with step definition parameters

我们看到的步骤定义代码可以运行,但它不太灵活。文本中嵌入了大量硬编码数据,当我们想要添加与此相关的新场景时,这可能会导致不必要的重复。例如,如果场景中提到的是银级常旅客而不是标准常旅客,我们需要编写另一个步骤定义方法:

The step definition code we saw will run, but it isn’t very flexible. There is a lot of hard-coded data embedded in the text, and this could lead to unnecessary duplication when we want to add new scenarios that are related to this one. For example, if the scenarios mentioned a Silver Frequent Flyer instead of a standard one, we would need to write another step definition method:

@Given(“Tara 是银卡常旅客会员”)
公共无效aSilverFrequentFlyerMember(){
  会员 = FrequentFlyerMember.named(“Tara”) 
                              .withStatus(FrequentFlyerStatus.Silver);
}
@Given("Tara is a Silver Frequent Flyer member")
public void aSilverFrequentFlyerMember() {
  member = FrequentFlyerMember.named("Tara") 
                              .withStatus(FrequentFlyerStatus.Silver);
}

当然,这是浪费。更好的方法是只使用一个步骤定义方法,并将常旅客类别传递给我们的自动化代码,如下所示。

Of course, this is wasteful. A better approach would be to have just one step definition method and to pass the Frequent Flyer category to our automation code, as follows.

清单 8.5 带有参数的步骤定义方法(Java 中)

Listing 8.5 A step definition method with a parameter (in Java)

飞行常客计划会员;                        
 
@Given("{word} 是 {word} 飞行常客计划会员")                            
public void aFrequentFlyerMember(String name, String status) {                
    FrequentFlyerStatus statusLevel = FrequentFlyerCategory.valueOf(状态); 
    会员 = FrequentFlyerMember.named(名称).withStatus(状态级别);
}
FrequentFlyerMember member;                        
 
@Given("{word} is a {word} Frequent Flyer member")                           
public void aFrequentFlyerMember(String name, String status) {               
    FrequentFlyerStatus statusLevel = FrequentFlyerCategory.valueOf(status); 
    member = FrequentFlyerMember.named(name).withStatus(statusLevel);
}

❶ 将状态定义为场景文本的变量部分,使用“{word}”。

The status is defined as a variable part of the scenario text, using “{word}”.

我们将 FrequentFlyer 状态作为字符串传递给我们的步骤定义方法。

We pass the FrequentFlyer status as a string to our step definition method.

我们将状态转换为可以传递给我们的构建器方法的枚举值。

We convert the status to an enum value that can be passed to our builder method.

在清单 8.5 的步骤定义注释中,我们将单词“Standard”或“Silver”替换为代表单个单词的特殊占位符:"{word}"。此符号称为Cucumber 表达式

In the step definition annotation in listing 8.5 we replaced the word “Standard” or “Silver” with a special placeholder that represents a single word: "{word}". This notation is known as a Cucumber Expression.

有许多 Cucumber 表达式可用,具体取决于要传递给步骤定义方法的数据类型和格式。例如,我们可以通过使用特殊的匿名 Cucumber 表达式类型("{}")来简化步骤定义代码,该类型会尽力将场景中的文本转换为您在步骤定义方法中定义的参数类型。匿名类型适用于枚举,因此我们可以"{word}"用匿名表达式替换清单 8.4 中使用的表达式,并使用枚举参数而不是字符串。您可以在以下清单中看到此重构的结果。

There are many Cucumber Expressions available, depending on the type and format of data you want to pass to your step definition method. For example, we could simplify the step definition code by using the special anonymous Cucumber Expression type ("{}"), which does its best to convert the text in the scenario to the parameter type you define in your step definition method. The anonymous type works fine for enums, so we could replace the "{word}" expression we used in listing 8.4 with an anonymous expression and use an enum parameter instead of a string. You can see the result of this refactoring in the following listing.

清单 8.6 使用匿名 Cucumber 表达式类型(在 Java 中)

Listing 8.6 Using the anonymous Cucumber Expression type (in Java)

飞行常客计划会员;                        
 
@Given("{word} 是 {} 飞行常客会员")                            
public void aStandardFrequentFlyerMember(String name,                     
                                         FrequentFlyerStatus 状态){    
    成员 = FrequentFlyerMember.named(名称) .withStatus (状态);
}
FrequentFlyerMember member;                        
 
@Given("{word} is a {} Frequent Flyer member")                           
public void aStandardFrequentFlyerMember(String name,                    
                                         FrequentFlyerStatus status) {   
    member = FrequentFlyerMember.named(name).withStatus(status);
}

"现在使用{}匿名类型定义状态"

The status is now defined using the "{}" anonymous type.

Cucumber 自动将状态文本转换为枚举值。

Cucumber converts the status text into the enum value automatically.

内置的 Cucumber 表达式列于表 8.1(另请参阅http://mng.bz/epzQ)。你甚至可以定义自己的(我们将在本章后面详细学习这一点)。

The built-in Cucumber expressions are listed in table 8.1 (also see http://mng.bz/epzQ). You can even define your own (we will learn more about this later in the chapter).

表 8.1 内置 Cucumber 表达式类型

Table 8.1 Built-in Cucumber Expression types

参数类型

Parameter Type

描述

Description

例子

Example

火柴

Matches

{int}

{int}

整数

A whole number

还有{int}剩余任务

There are {int} remaining tasks

还剩 5 项任务

There are 5 remaining tasks

{float}

{float}

浮点数

A floating-point number

该箱子重{float}公斤

The box weighs {float} kg

该箱重7.5公斤

The box weighs 7.5 kg

{word}

{word}

一个单词(无空格)

A single word (without whitespace)

她买了一件{word}球衣

She buys a {word} jersey

她买了一件蓝色运动衫

She buys a blue jersey

{string}

{string}

用双引号括起来的字符串

A string enclosed in double-quotes

她飞往{string}

She flies to {string}

她飞往“纽约”

She flies to “New York”

{}

{}

匹配任何表达式(适用于枚举和大多数基本类型)

Matches any expression (works with enums and most basic types)

她飞往{}

She flies to {}

她飞往纽约

She flies to New York

{biginteger}(仅适用于使用 JVM 语言编写的步骤定义

{biginteger} (only applies to step definitions written in a JVM language)

一个BigInteger参数

A BigInteger parameter

人口{biginteger}

A population of {biginteger}

人口50亿

A population of 5,000,000,000

{bigdecimal}

{bigdecimal}

一个BigDecimal参数

A BigDecimal parameter

总价值{bigdecimal}

A total value of {bigdecimal}

总价值 99.9876543

A total value of 99.9876543

{byte}

{byte}

一个Byte参数

A Byte parameter

{byte}苹果

There are {byte} apples

有 500 个苹果

There are 500 apples

{short}

{short}

一个Short参数

A Short parameter

{short}苹果

There are {short} apples

有 5,000 个苹果

There are 5,000 apples

{long}

{long}

一个Long参数

A Long parameter

{long}苹果

There are {long} apples

有 50,000,000 个苹果

There are 50,000,000 apples

{double}

{double}

一个Double参数

A Double parameter

该箱子重{double}公斤

The box weighs {double} kg

该箱子重7.4987公斤

The box weighs 7.4987 kg

生成步骤定义

Generating your step definitions

Cucumber 为您提供了几种连接 Gherkin 场景并将其绑定到测试自动化代码的方法。首先,当您为没有步骤定义的场景运行测试运行器类时,Cucumber 会将一些示例步骤粘合代码写入控制台输出:

Cucumber gives you a couple of ways to wire your Gherkin scenarios and bind them to your test automation code. First, when you run a test runner class for a scenario with no step definitions, Cucumber will write some sample step glue code to the console output:

$ mvn 验证
...
存在未定义的步骤。您可以使用 
片段如下:
@Given(“Tara 是一名飞行常客”)
公共无效tara_is_a_Frequent_Flyer_traveler(){
    // 在此处编写代码,将上面的短语转换为具体的操作
     抛出新的 cucumber.api.PendingException();
}
@Then(“她应该获得 {int} 分”)
public void she_should_earn_points(Integer int1){
    // 在此处编写代码,将上面的短语转换为具体的操作
    抛出新的 cucumber.api.PendingException();
}
...
$ mvn verify
...
There were undefined steps. You can implement missing steps with the 
 snippets below:
@Given("Tara is a Frequent Flyer traveler")
public void tara_is_a_Frequent_Flyer_traveler() {
    // Write code here that turns the phrase above into concrete actions
     throw new cucumber.api.PendingException();
}
@Then(„she should earn {int} points“)
public void she_should_earn_points(Integer int1) {
    // Write code here that turns the phrase above into concrete actions
    throw new cucumber.api.PendingException();
}
...

一个更方便的选项,在 IntelliJ IDEA 和 Eclipse 中均可用(通过 Cucumber Eclipse 插件),即直接从您的 IDE 运行功能或场景(在撰写本文时,IntelliJ IDEA 是唯一支持从 IDE 执行单个场景的 IDE)。这将导致任何未定义步骤的步骤定义片段被写入控制台。

A more convenient option, available in both IntelliJ IDEA and Eclipse (via the Cucumber Eclipse plug-in), is to run the feature or scenario (at the time of writing IntelliJ IDEA is the only IDE that supports the execution of individual scenarios from the IDE) itself directly from your IDE. This will cause the step definition snippets for any undefined steps to be written to the console.

然后,您可以将这些代码片段复制到步骤定义类中。这些代码片段是一个很好的开始,但您通常需要对它们进行一些调整;它们可能会缺少参数,至少,您需要使参数名称更具可读性。

You can then copy these snippets into your step definition classes. These snippets are a good start, but you will usually need to tweak them a bit; they may miss parameters, and at the very least, you will need to make parameter names more readable.

IntelliJ IDEA 和 Eclipse(通过 Cucumber Eclipse 插件)等 IDE 允许您使用上下文菜单直接从 IDE 生成步骤定义代码。

IDEs such as IntelliJ IDEA and Eclipse (via the Cucumber Eclipse plug-in) allow you to generate step definition code directly from your IDE, using the contextual menu.

8.4.2 让你的 Cucumber 表达式更加灵活

8.4.2 Making your Cucumber Expressions more flexible

可执行文件规范首先是一种文档形式,以业务可读的语言编写。这就是为什么最好的 Gherkin 场景和功能文件读起来非常像普通的规范文档,以流畅、可读的业务语言编写。

Executable specifications are above all a form of documentation, written in business-readable language. This is why the best Gherkin scenarios and feature files read very much like a normal specifications document, in fluent, readable business language.

语法细节可以大大提高场景的可读性。如果语法不一致,读者可能无法集中注意力于场景背后的业务意图,从而导致场景在传达信息方面效率降低。例如,请考虑以下步骤定义:

Little grammatical details can go a long way to make your scenarios easier to read. When there are inconsistencies in grammar, it can distract the reader from focusing on the business intent behind the scenario and make the scenario a little bit less effective at communicating its message. For example, consider the following step definition:

@Then(“她已经成为会员 {int} 年了”)
@Then("she has been a member for {int} years")

这将匹配“她已经成为会员 5 年了”和“她已经成为会员 1 年了”,但不是“她已经成为会员 1 年了”。但是,我们可以让 Cucumber 接受这两种形式,方法是将最后的“s”设为可选项。使用 Cucumber 表达式时,您可以通过将任何文本括在括号中来使其成为可选项,如下例所示:

This will match both “she has been a member for 5 years” and “she has been a member for 1 years,” but not “she has been a member for 1 year.” However, we could tell Cucumber to accept both forms by making the final “s” optional. When using Cucumber Expressions, you can make any text optional by surrounding it in parentheses, like in the following example:

@Then(“她已成为会员 {int} 年”)
@Then("she has been a member for {int} year(s)")

可选文本(英文)的另一个常见用途是使场景性别更加灵活:

Another common use of optional text (in English) is to make scenario genders a bit more flexible:

@Then(“他已经成为会员 {int} 年了”)
@Then("(s)he has been a member for {int} year(s)")

这将匹配以下两种变体:

This would match both of the following variations:

  • “她已经成为会员 5 年了”

  • “she has been a member for 5 years”

  • “他已经成为会员一年了”

  • “he has been a member for 1 year”

有时,允许场景文本中存在细微的变化会很有帮助。例如,假设业务用户使用“成员”和“客户”这两个词的同义词。Cucumber Expressions 允许我们使用斜线字符来指示场景文本中的替代词。您可以在此处看到一个示例:

Sometimes it can be helpful to allow small variations in the scenario text. For example, suppose that business users use the words “member” and “customer” synonymously. Cucumber Expressions allow us to use the slash character to indicate alternative words in a scenario text. You can see an example here:

@Then(“他已经成为会员/客户 {int} 年了”)
@Then("(s)he has been a member/customer for {int} year(s)")

此黄瓜表达式在以下所有情况下均有效:

This Cucumber Expression would work in all of the following cases:

  • “她已经成为会员一年了”

  • “she has been a member for 1 year”

  • “他已经是我们的客户 5 年了”

  • “he has been a customer for 5 years”

请注意,这只适用于没有空格的单词;对于更复杂的选项,你需要使用正则表达式,我们将在后面介绍章。

Note that this will only work for words without whitespaces; for more sophisticated options, you need to use regular expressions, which we will look at later in this chapter.

8.4.3 Cucumber 表达式和自定义参数类型

8.4.3 Cucumber Expressions and custom parameter types

黄瓜表达式旨在使您的步骤定义更具可读性。实现此目的并在此过程中简化代码的一种好方法是使用自定义参数类型。

Cucumber expressions are meant to make your step definitions more readable. One great way to do this, and to simplify your code in the process, is to use custom parameter types.

自定义参数类型允许您使用自己的域相关参数类型使步骤定义表达式更有意义。例如,在前面的一个步骤定义中,我们使用匿名参数类型传入常旅客状态,然后创建常旅客会员:

Custom parameter types allow you to make your step definition expressions more meaningful by using your own domain-related parameter types. For example, in one of the previous step definitions, we used an anonymous parameter type to pass in the frequent flyer status and then create a Frequent Flyer member:

@Given("{} 是 {} 飞行常客会员")                
public void aFrequentFlyerMember(String name, FrequentFlyerStatus status) {    
    会员 = FrequentFlyerMember.newMember()
                                .named(名称)
                                .withStatus(状态);
}
@Given("{} is a {} Frequent Flyer member")                
public void aFrequentFlyerMember(String name, FrequentFlyerStatus status) {    
    member = FrequentFlyerMember.newMember()
                                .named(name)
                                .withStatus(status);
}

我们可以通过定义一个参数化类型来表示常旅客会员,从而使此代码更具可读性和可重用性。这将允许我们传递一个FrequentFlyerMember参数直接进入我们的步骤定义方法,如下所示:

We could make this code a little more readable and a little more reusable by defining a parameterized type to represent Frequent Flyer members. This would allow us to pass a FrequentFlyerMember parameter directly into our step definition method, like this:

@Given("{} 是 {frequentFlyer}")
public void aFrequentFlyerMember(String name, FrequentFlyerMember member) {
    this.member = member.named(name);
}
@Given("{} is a {frequentFlyer}")
public void aFrequentFlyerMember(String name, FrequentFlyerMember member) {
    this.member = member.named(name);
}

为了实现这一点,我们需要一种方法来识别与常旅客会员描述相对应的文本(例如,“标准常旅客会员”或“银级常旅客会员”),并将此文本转换为正确的参数类型。我们使用ParameterType注释识别如下清单中的方法。(ParameterType方法可以进入你的步骤定义类或者粘合代码包中的任何其他类。)

To make this work, we need a method that recognizes the text corresponding to the description of the Frequent Flyer member (e.g., “Standard Frequent Flyer Member” or “Silver Frequent Flyer member”) and that converts this text into the correct parameter type. We use the ParameterType annotation to identify methods like in the following listing. (ParameterType methods can go in your step definition classes or in any other class in your glue code packages.)

清单 8.7 参数类型定义

Listing 8.7 A parameter type definition

@ParameterType("(标准|银|铜|金) 飞行常客会员")       
公共 FrequentFlyerMember oftenFlyer(字符串状态名称){            
    FrequentFlyerStatus 状态 = FrequentFlyerStatus.valueOf(状态名称);   
    返回 FrequentFlyerMember.withStatus(status);                          
}
@ParameterType("(Standard|Silver|Bronze|Gold) Frequent Flyer member")      
public FrequentFlyerMember frequentFlyer(String statusName) {            
    FrequentFlyerStatus status = FrequentFlyerStatus.valueOf(statusName);  
    return FrequentFlyerMember.withStatus(status);                         
}

这是一个标识参数值的正则表达式(稍后会详细介绍)。

This is a regular expression (more about these later) that identifies the parameter value.

我们将状态作为字符串传递给参数。

We pass the status into the parameter as a String.

我们将返回具有此状态的飞行常客会员。

We return a Frequent Flyer member with this status.

ParameterType方法定义一个正则表达式来识别场景中应该转换为对象的文本片段,并定义允许的值。参数类型的名称源自方法的名称(如果您愿意,也可以使用 name 属性)。

ParameterType methods define a regular expression to identify the piece of text in the scenario that should be converted to an object, and that defines the permitted values. The name of the parameter type is derived from the name of the method (you can also use the name attribute if you prefer).

当我们像这样定义领域特定参数类型时,很容易在其他步骤定义方法中重用它们。例如,假设我们需要实现以下场景:

When we define domain-specific parameter types like this, it is easy to reuse them in other step definition methods. For example, suppose we need to implement the following scenario:

场景:已拥有 10,000 积分的会员可获得 50% 奖励积分
  鉴于Tara 是银卡常旅客会员,拥有 20000 积分
  她完成从巴黎飞往柏林的航班
时  那么她应该获得 150 分
Scenario: 50% bonus points for members who already have 10000 points
  Given Tara is a Silver Frequent Flyer member with 20000 points
  When she completes a flight from Paris to Berlin
  Then she should earn 150 points

使用frequentFlyer自定义参数类型我们之前定义过,相应的步骤定义就变得简单多了。

Using the frequentFlyer custom parameter type we defined earlier, the corresponding step definition becomes much simpler.

清单 8.8 另一个使用自定义参数类型的步骤定义方法

Listing 8.8 Another step definition method using a custom parameter type

@Given("{} 是 {frequentFlyer},拥有 {int} 积分")                        
公共无效aFrequentFlyerMemberWithPoints(字符串名称,
                                           常旅客会员,     
                                           int 点){
    this.member = member.named(name).withPoints(points);
}
@Given("{} is a {frequentFlyer} with {int} points")                       
public void aFrequentFlyerMemberWithPoints(String name,
                                           FrequentFlyerMember member,    
                                           int points) {
    this.member = member.named(name).withPoints(points);
}

常旅客会员及其积分

A Frequent Flyer member along with their points

常旅客会员转换为 FrequentFlyerMember 参数。

The Frequent Flyer member is converted to a FrequentFlyerMember parameter.

我们还可以使用更精确的正则表达式来定义更复杂的自定义类型。例如,Cucumber 没有内置的日期格式,但定义一个很容易。假设我们想要自动执行以下步骤:

We can also use more precise regular expressions to define more sophisticated custom types. For example, Cucumber has no built-in date format, but it is easy to define one. Suppose we wanted to automate the following step:

鉴于Todd 于 2020-01-01 加入了飞行常客计划
Given Todd joined the Frequent Flyer program on 2020-01-01

我们可以使用与我们要使用的日期格式匹配的正则表达式来定义一个自定义日期类型,例如“ISO-Date”。与此日期格式匹配的正则表达式可以是"\d{4}-\d{2}-\d{2}"(四位数字后跟一个破折号,然后两位数字后跟一个破折号,然后再两位数字)。我们还可以使用 name 属性来定义不遵循 Java 方法名称约定的参数类型。我们的参数类型方法如下所示。

We could define a custom date type called, for example, “ISO-Date,” using a regular expression that matches the date format we want to use. A regular expression that matches this date format could be "\d{4}-\d{2}-\d{2}" (four digits followed by a dash, then two digits followed by a dash, then two more digits). We can also use the name attribute to define parameter types that don’t follow a Java method name convention. Our parameter type method would like the following.

清单 8.9 日期的自定义参数类型

Listing 8.9 A custom parameter type for a date

@ParameterType(name =“ISO-date”,                      
               值="(\\d{4}-\\d{2}-\\d{2})")       
公共 LocalDate isoDate(字符串 formattedDate){
    返回 LocalDate.parse (formattedDate)
 ;}
@ParameterType(name="ISO-date",                     
               value="(\\d{4}-\\d{2}-\\d{2})")      
public LocalDate isoDate(String formattedDate) {
    return LocalDate.parse(formattedDate);
}

参数类型的名称

The name of the parameter type

匹配该类型参数的正则表达式

The regular expression that matches this type of parameter

步骤定义代码将看起来像这样:

The step definition code would then look something like this:

@Given("{} 于 {ISO-date} 加入了飞行常客计划")
public void justJoined(String name, LocalDate date) {...}
@Given("{} joined the Frequent Flyer programme on {ISO-date}")
public void justJoined(String name, LocalDate date) {...}

到目前为止,我们已经多次提到正则表达式,但还没有详细研究过它们。在下一节中,我们将仔细研究什么是正则表达式,以及如何在黄瓜。

So far, we have alluded to regular expressions a few times, but not looked at them in much detail. In the next section we will take a closer look at what regular expressions are and how we use them in Cucumber.

8.4.4 使用正则表达式

8.4.4 Using regular expressions

黄瓜表达式并不是编写步骤定义方法的唯一方法。您还可以使用正则表达式,它为字符串中的模式匹配提供了简洁的语法。就像 Cucumber 表达式一样,您可以使用正则表达式来识别场景中的参数。在旧版本的 Cucumber 中,正则表达式是实现步骤定义的唯一方法,因此您可能会在旧代码库中看到很多正则表达式。正如我们在上一节中看到的,我们在定义自定义参数类型时也会使用正则表达式。您可以在下面的清单中看到我们使用正则表达式的上一个步骤定义的示例。

Cucumber Expressions are not the only way to write your step definition methods. You can also use regular expressions, which provide a concise syntax for pattern matching in strings. Just like Cucumber Expressions, you can use regular expressions to identify parameters inside your scenario. In older versions of Cucumber, regular expressions were the only way to implement step definitions, so you may see a lot of them in older code bases. And as we saw in the previous section, we also use regular expressions when we define custom parameter types. You can see an example of the previous step defintion we saw, using regular expressions, in the following listing.

清单 8.10 使用正则表达式编写的步骤定义

Listing 8.10 A step definition written using regular expressions

@Given("^Tara 是 (.*) 飞行常客计划会员,拥有 (\\d+) 积分")        
公共无效aFrequentFlyerMemberWithPoints(FrequentFlyerStatus状态,int点){
    会员 = FrequentFlyerMember.withStatus (状态)
 ;    成员.setPoints(点);
}
@Given("^Tara is a (.*) Frequent Flyer member with (\\d+) points")       
public void aFrequentFlyerMemberWithPoints(FrequentFlyerStatus status, int points) {
    member = FrequentFlyerMember.withStatus(status);
    member.setPoints(points);
}

使用正则表达式识别状态和积分参数。

Regular expressions are used to identify the status and points parameters.

编写良好的正则表达式可以大大提高步骤定义灵活性和可重用性。正则表达式非常强大,但也因难以理解而臭名昭著。早在 20 世纪 90 年代末,Netscape 工程师 Jamie Zawinski 就幽默地评论道:“有些人在遇到问题时会想,‘我知道,我会使用正则表达式。’现在他们有两个问题。”虽然随着 Cucumber Expressions 的引入,您不太需要它们,但它们仍然会不时派上用场。幸运的是,编写有效的步骤定义方法所需了解的正则表达式的范围和复杂性相对有限。

Well-written regular expressions can go a long way to making your step definitions more flexible and more reusable. Regular expressions are extremely powerful but are also notorious for being hard to understand. Back in the late 1990s Netscape engineer Jamie Zawinski humorously remarked, “Some people, when confronted with a problem, think, ‘I know, I’ll use regular expressions.’ Now they have two problems.” Although you need them less often with the introduction of Cucumber Expressions, they still come in handy from time to time. Fortunately, the range and complexity of the regular expressions you need to know to write effective step definition methods are relatively limited.

简单匹配器

Simple matchers

Cucumber 步骤定义中最简单、最常用的正则表达式是通配符(.*)。它读作“任意字符(换行符除外),任意次数”,因此以下注释将与我们之前看到的文本匹配:“她已成为会员 5 年了”:

The simplest and most commonly used regular expression for Cucumber step definitions is the wildcard (.*). This reads as “any character (except for a new line), any number of times,” so the following annotation would match the text we saw earlier: “she has been a member for 5 years”:

@Then("^她已经成为会员 (.*) 年了")
@Then("^she has been a member for (.*) years")

让我们详细分析一下。

Let’s break this down.

在正则表达式中,我们使用插入^符号 ( ) 来表示字符串的开头。这可以使表达式更清晰,并且某些版本的 Cucumber 会使用它来了解何时要使用正则表达式而不是 Cucumber 表达式。

In regular expressions, we use the caret (^) symbol to indicate the start of the string. This makes the expression less ambiguous, and some versions of Cucumber use this to know when you want to use regular expressions as opposed to Cucumber Expressions.

正则表达式(.*)被括号括起来。这被称为组,Cucumber 正是通过组来知道将文本的哪些部分转换为参数变量。

The regular expression (.*) is surrounded by parentheses. This is known as a group and is how Cucumber knows which parts of the text to convert into parameter variables.

匹配特定类型的字符

Matching specific types of characters

也可以匹配特定范围或类型的字符。例如,在正则表达式中,“d”代表数字,加号读作“一个或多个”,因此正则表达式“d+”将匹配一个或多个数字的序列。我们需要用反斜杠转义字母符号,因此我们可以重写我们的场景定义,如下所示这:

You can also match specific ranges or types of characters. For example, in regular expressions, “d” represents a digit, and the plus symbol reads as “one or more,” so the regular expression “d+” would match a sequence of one or more digits. We need to escape alphabetical symbols with backslashes, so we could rewrite our scenario definition like this:

@Given("^她已成为会员 (\\d+) 年")
@Given("^she has been a member for (\\d+) years")

匹配可能值的集合

Matching sets of possible values

正则表达式还允许您使用竖线分隔的列表(如下所示)定义一组可能的值

Regular expressions also let you define a set of possible values, using a pipe-separated list like the one shown here.

@Given ("^Tara 是 (标准|青铜|白银|黄金) 飞行常客会员")
公共无效aFrequentFlyerMember(FrequentFlyerStatus状态){...}
@Given ("^Tara is a (Standard|Bronze|Silver|Gold) Frequent Flyer member")
public void aFrequentFlyerMember(FrequentFlyerStatus status) {...}

可选字符

Optional characters

我们之前看到过,使用 Cucumber 表达式,你可以通过将字符括在括号中来使它成为可选字符;例如,“(s)he” 是一个 Cucumber 表达式,可以匹配“she”和“he”。我们可以使用正则表达式通过使用问号来做同样的事情,如下所示这里:

We saw earlier how with Cucumber Expressions you can make a character optional by surrounding it in parentheses; for example, “(s)he” is a Cucumber Expression that will match both “she” and “he.” We can do the same thing using regular expressions by using a question mark, as shown here:

@Given("^s?他已经成为会员 (\\d+) 年了")
@Given("^s?he has been a member for (\\d+) years")

非捕获组

Non-capturing Groups

有时我们需要支持文本中的几种措辞变体,这些变体与我们传递给步骤定义方法的参数无关。例如,假设我们想要匹配“当 Tara 在网站上注册时”和“假设 Tara 已在网站上注册”。使用正则表达式,我们可以使用非捕获组来执行此操作。非捕获组是将被匹配但不会传递给步骤定义方法的正则表达式。它们以 为前缀?:,如下所示例子:

Sometimes we need to support several variations of wording in a text, which are unrelated to the parameters that we pass to the step definition methods. For example, suppose we wanted to match both “when Tara registers on the site” and “given Tara has registered on the site.” Using regular expressions, we can do this using a noncapturing group. Non-capturing groups are regular expressions that will be matched but that will not be passed through to the step definition method. They are prefixed by ?:, as shown in this example:

@Then("^(.*) (?:registers|已在网站上注册)")
公共无效用户已注册(字符串名称){...}
@Then("^(.*) (?:registers|has registered) on the site")
public void userHasRegistered(String name){...}

常见正则表达式

Common regular expressions

可以看到更完整的更有用的正则表达式列表(以及我们已经看到的正则表达式的回顾)在 t能够 8.2。

You can see a more complete list of the more useful regular expressions (and a recap of the ones we have already seen) in table 8.2.

表 8.2 常见正则表达式Cucumber 场景中使用的表达式

Table 8.2 Common regular expressions used in Cucumber scenarios

正则表达式

Regular Expression

描述

Description

例子

Example

火柴

Matches

.*

.*

任意字符序列(包括无字符)

Any sequence of characters (including nothing)

^ (.*)是常旅客

^ (.*) is a frequent flyer

Tara 是一名常旅客

Tara is a frequent flyer

.+

.+

一个或多个字符的序列

A sequence of one or more characters

^ (.+)是常旅客

^ (.+) is a frequent flyer

Tara 是一名常旅客

Tara is a frequent flyer

/d+

/d+

一个或多个数字的序列

A sequence of one or more digits

Tara 今年 ( /d+) 岁

Tara is (/d+) years old

塔拉 30 岁

Tara is 30 years old

/d{n}

/d{n}

恰好 n 位数字的序列

A sequence of precisely n digits

在一年中/d{4}

In the year /d{4}

2020 年

In the year 2020

(a|b|c)

(a|b|c)

列出的任意一个值

Any one of the listed values

状态为(red|amber|green

The status is (red|amber|green)

状态为红色

The status is red

状态为琥珀色

The status is amber

状态为绿色

The status is green

?

?

可选字符

An optional character

s?he购买机票

s?he purchases a ticket

她买了一张票

she purchases a ticket

他买了一张票

he purchases a ticket

?:

?:

非捕获组

A non-capturing group

塔拉(?:buys|has bought) 一张票

Tara (?:buys|has bought) a ticket

塔拉买了一张票

Tara buys a ticket

塔拉已经买了票

Tara has bought a ticket

8.4.5 使用列表和数据表

8.4.5 Working with lists and data tables

所以到目前为止,我们一直在研究简单的数据类型。但我们经常需要传递更复杂的数据结构。

So far, we have been looking at simple data types. But very often we need to pass in more complex data structures.

使用简单列表

Working with simple lists

简单的列表(只有一列的表)通常可以自动映射。例如,以下场景包含一个简单的字符串列表:

Simple lists (tables with a single column) can generally be mapped automatically. For example, the following scenario contains a simple list of strings:

可用的目的地应该是
  | 柏林 |
  | 巴黎 |
  | 纽约 |
And the available destinations should be:
  | Berlin   |
  | Paris    |
  | New York |

相应的步骤定义方法将简单地接受一个字符串列表作为范围:

The corresponding step definition method would simply accept a list of strings as a parameter:

@Then("可用的目的地应该是")
公共无效可用目的地(列表<String>目的地){...}
@Then("the available destinations should be")
public void availableDestinations(List<String> destinations) {...}

使用嵌入列表

Working with embedded lists

有时在步骤文本中嵌入一个简短的值列表,而不是在单独的列表中,这样会更具可读性,如您在此示例中所见:

Sometimes it can be more readable to embed a short list of values inside the step text, rather than in a separate list, as you can see in this example:

那么可用的目的地应该是柏林、巴黎、纽约
Then the available destinations should be Berlin, Paris, New York

实现此目的的最简单方法是定义一个参数类型方法,将文本中的字符串列表转换为实际的字符串列表。(在实际代码中,您甚至可以使用以下清单中显示的相同技术将值转换为枚举或域对象列表。)

The simplest way to achieve this is to define a parameter type method that converts the list of strings in the text to an actual list of strings. (In real-world code you might even convert the values to a list of enums or domain objects using the same technique shown in the following listing.)

清单 8.11 处理字符串列表的参数类型

Listing 8.11 A parameter type that handles a list of strings

@ParameterType(名称 =“字符串值”,值 =“(.*)”)
公共列表<String> stringValues(String destinationList) {
    返回 Stream.of(destinationList.split(","))             
                 .map(String :: trim)                          
                 .收集(Collectors.toList());              
}
@ParameterType(name = "string-values", value = "(.*)")
public List<String> stringValues(String destinationList) {
    return Stream.of(destinationList.split(","))            
                 .map(String::trim)                         
                 .collect(Collectors.toList());             
}

将列表拆分成单个元素

Splits the list into its individual elements

修剪所有空白

Trims off any white spaces

以字符串列表形式返回结果

Returns the result as a list of strings

步骤定义代码现在看起来像这样:

The step definition code would now look like this:

@Then("可用的目的地应该是 {string-values}")
公共无效可用目的地(列表<String>目的地){...}
@Then("the available destinations should be {string-values}")
public void availableDestinations(List<String> destinations) {...}

列表很方便,但我们经常需要表达更复杂的数据结构,而一个很好的方法是使用表。

Lists are convenient, but we often need to express more involved data structures, and a great way to do this is using tables.

使用表格

Working with tables

认为我们有以下场景:

Suppose we have the following scenario:

场景:可以申领过去 90 天内完成的航班
  鉴于Todd 于 2020-01-01 加入了飞行常客计划
  Todd 要求将以下航班费用记入他的账户时:
    | 航班号 | 日期 | 状态 |
    | FH-101 | 2019-12-01 | 已完成 |
    | FH-102 | 2019-12-01 | 已取消 |
    | FH-103 | 2019-08-01 | 已完成 |
  那么仅应记入以下航班:
    | 航班号 | 日期 | 状态 |
    | FH-101 | 2019-12-01 | 已完成 |
Scenario: Completed flights in the past 90 days can be claimed
  Given Todd joined the Frequent Flyer program on 2020-01-01
  When Todd asks for the following flight to be credited to his account:
    | Flight Number | Date       | Status    |
    | FH-101        | 2019-12-01 | COMPLETED |
    | FH-102        | 2019-12-01 | CANCELED |
    | FH-103        | 2019-08-01 | COMPLETED |
  Then only the following flights should be credited:
    | Flight Number | Date       | Status    |
    | FH-101        | 2019-12-01 | COMPLETED |

在第二步中,我们传递一个包含多个航班详细信息的数据表。在第三步中,我们传递一个我们期望记入 Todd 的飞行常客帐户的航班列表。

In the second step, we pass a data table containing the flight details for a number of flights. In the third step, we pass a list of the flights we expect to be credited to Todd’s Frequent Flyer account.

表中的每一行代表 Todd 完成的一次航班。我们可以使用一个简单的域类来表示已完成航班的概念,如下所示。

Each row in the table represents one of the flights that Todd has completed. We could represent the concept of completed flights using a simple domain class, as follows.

清单 8.12 表示过去航班的领域类

Listing 8.12 A domain class representing a past flight

导入java.time.LocalDate;
 
公共记录 PastFlight(字符串 flightNumber, 
                         本地日期预定日期, 
                         FlightStatus 状态){}
import java.time.LocalDate;
 
public record PastFlight(String flightNumber, 
                         LocalDate scheduledDate, 
                         FlightStatus status) {}

DataTable默认情况下,Cucumber 将使用其自己的类将表传递到步骤定义方法中。您可以使用该类提取表的行和列,如下面的清单所示。

By default, Cucumber will pass tables into step definition methods using its own DataTable class. You can use this class to extract the rows and columns of the table, as in the following listing.

清单 8.13 使用DataTable

Listing 8.13 Using the DataTable class

@When("{} 要求将以下航班的费用记入他的账户:")
public void creditFlights(String name, DataTable flights) {       
    List<PastFlight> requestedFlights = flights.asMaps()          
            。溪流()
            .map(row -> new PastFlight(                           
                    row.get("航班号"),
                    LocalDate.parse ( row.get ("Date")),
                    FlightStatus.valueOf (row.get(" Status "))))
.collect (             Collectors.toList ());                        
...
}
@When("{} asks for the following flight to be credited to his account:")
public void creditFlights(String name, DataTable flights) {      
    List<PastFlight> requestedFlights = flights.asMaps()         
            .stream()
            .map(row -> new PastFlight(                          
                    row.get("Flight Number"),
                    LocalDate.parse(row.get("Date")),
                    FlightStatus.valueOf(row.get("Status"))))
            .collect(Collectors.toList());                       
...
}

Cucumber 将表的行和单元格值作为 DataTable 对象提供。

Cucumber provides the table’s rows and cell values as a DataTable object.

将行和单元格提取为地图列表

Extracts the rows and cells as a list of maps

将每张地图转换为 PastFlight 域对象

Converts each map into a PastFlight domain object

以列表形式返回 PastFlight 对象的集合

Returns the collection of PastFlight objects as a list

但是,这段代码的可读性不强,而且比我们通常在步骤定义方法中预期的要复杂得多。如果我们可以将有意义的域对象列表直接传递给步骤定义方法,那么它将更加简洁,也更易于维护。

However, this code is not very readable and is more complex than we would typically expect in a step definition method. It would be much cleaner and easier to maintain if we could pass in a list of meaningful domain objects directly to the step definition method.

我们可以通过将转换逻辑重构为单独的方法来实现这一点,我们使用DataTableType注释进行注释。此注释告诉 Cucumber 如何将数据表转换为域对象或域对象列表。您可以在以下清单中看到如何使用此注释的示例。

We can do this by refactoring the conversion logic into a separate method, which we annotate with the DataTableType annotation. This annotation tells Cucumber how to convert data tables into domain objects or lists of domain objects. You can see an example of how to use this annotation in the following listing.

清单 8.14 使用DataTableType注释

Listing 8.14 Using the DataTableType annotation

@DataTableType
公共 PastFlight mapRowToPastFlight(Map <String,String> entry){      
    return new PastFlight(entry.get("航班号"),                  
                          LocalDate.解析(entry.get("日期")),
                          航班状态.valueOf(entry.get("状态")));
}
@DataTableType
public PastFlight mapRowToPastFlight(Map<String, String> entry) {     
    return new PastFlight(entry.get("Flight Number"),                 
                          LocalDate.parse(entry.get("Date")),
                          FlightStatus.valueOf(entry.get("Status")));
}

Cucumber 提供单行作为字符串的映射。

Cucumber provides a single row as a map of Strings.

我们将行转换为我们需要的任何域对象。

We convert the row to whatever domain object we need.

此方法可以放在粘合代码中的任何位置 - 例如,在步骤定义类中与使用PastFlight域对象的方法一起或者(如果域对象在许多地方使用)它可能会进入它自己的类中。

This method can go anywhere in your glue code—for example, in the step definition class alongside the methods that use the PastFlight domain objects, or (if the domain object is used in many places) it might go in a class of its own.

PastFlight我们现在可以传递域对象列表直接到我们的步骤定义方法,例如这:

We can now pass a list of PastFlight domain objects directly to our step definition method, like this:

@When(“{word} 要求将以下航班记入他的账户:”)
公共无效creditFlights(String name,List <PastFlight> requestedFlights){
    ...                        
}    
@When("{word} asks for the following flight to be credited to his account:")
public void creditFlights(String name, List<PastFlight> requestedFlights) {
    ...                        
}    

处理更复杂的表格

Working with more complex tables

转换以这种方式将 Gherkin 表转换为域类型确实需要付出一些额外的努力,但从可读性和可维护性的角度来看,回报非常可观。然而,有时您需要描述的数据会更复杂一些。在现实世界中,您可能需要处理具有许多字段的复杂域对象,但相关字段因场景而异。过去的航班记录可能包含比我们看到的三个更多的字段,包括对这些场景无用的字段。

Converting Gherkin tables to domain types this way does take a little extra effort initially but pays off handsomely in terms of readability and maintainability down the line. Sometimes, however, the data you need to describe is a bit more complex. In real-world scenarios, you may need to deal with complex domain objects with many fields, but where the relevant fields vary from one scenario to another. A past flight record could include many more fields than the three we saw, including fields that are not useful for these scenarios.

例如,假设您现在需要实现以下场景:

For example, suppose you now need to implement the following scenario:

情景概述:常旅客会员因迟到而获得额外积分 
航班
1 小时后,每延迟 1 小时可额外获得 10 分。
  鉴于Tracy 曾乘坐过以下航班:
    | 航班号 | 延误 | 延误者 | 额外积分 |
    | <航班号> | <延误> | <延误者> | <额外积分> |
  机票被记入她的账户
时  那么她应该被记为<Extra Points>额外积分
  例如    | 航班号 | 延误 | 延误者 | 额外积分 |
    | FH-101 | 否 | | 0 |
    | FH-102 | 是 | 10米 | 0 |
    | FH-102 | 是 | 60米 | 10 |
    | FH-103 | 是 | 2小时30分钟 | 20 |
    | FH-104 | 是 | 5小时 | 50 |
Scenario Outline: Frequent flyer members are awarded extra points for late 
 flights
10 extra points are earned for each hour delayed after 1 hour.
  Given Tracy has traveled on the following flights:
    | Flight Number   | Delayed   | Delayed By   | Extra Points   |
    | <Flight Number> | <Delayed> | <Delayed By> | <Extra Points> |
  When the flight is credited to her account
  Then she should be credited with <Extra Points> additional points
  Examples:
    | Flight Number | Delayed | Delayed By | Extra Points |
    | FH-101        | No      |            | 0            |
    | FH-102        | Yes     | 10m        | 0            |
    | FH-102        | Yes     | 60m        | 10           |
    | FH-103        | Yes     | 2h30m      | 20           |
    | FH-104        | Yes     | 5h         | 50           |

在这个需求中,我们使用了不同的字段子集。航班的日期和状态与此场景无关,但我们确实需要知道每个航班延误了多长时间。

In this requirement, we use a different subset of fields. The date and status of the flights are not relevant for this scenario, but we do need to know how long each flight was delayed.

班级PastFlight需要一些额外的字段来支持这一要求。您可以在下面的清单中看到一种可能的实现。

The PastFlight class needs some additional fields to support this requirement. You can see one possible implementation in the following listing.

清单 8.15 表示过去航班的域类,带有附加字段

Listing 8.15 A domain class representing a past flight with additional fields

公共记录 PastFlight(字符串 flightNumber,
                         本地日期预定日期,
                         FlightStatus 状态,
                         布尔值 wasDelayed,
                         经期延迟,
                         字符串登机牌号码){}
public record PastFlight(String flightNumber,
                         LocalDate scheduledDate,
                         FlightStatus status,
                         Boolean wasDelayed,
                         Period delayedBy,
                         String boardingPassNumber) {}

现在棘手的事情是赋予步骤定义方法所需的灵活性,以便能够应对不同的表结构。一种方法可能是检查每个字段,如果没有提供,则使用合理的默认值。在下面的清单中,您可以看到这种方法的示例实现。

The tricky thing now is to give your step definition methods the flexibility they need to be able to cope with different table structures. One approach might be to check each field and use a sensible default value if none is provided. In the following listing, you can see a sample implementation of this approach.

清单 8.16 表示过去航班的域类,带有附加字段

Listing 8.16 A domain class representing a past flight with additional fields

@DataTableType
公共 PastFlight mapRowToPastFlight(Map <String,String> entry){
    返回新的 PastFlight(
            可选(entry.get(“航班号”),“FT-101”),
            LocalDate.parse (optional( entry.get ("Date"), "2020-10-01")),
            FlightStatus.valueOf (optional(entry.get("Status"), "COMPLETED"))
 ,            是否延迟(条目),
            延迟时间(条目));
}
 
私有<T> T 可选(T cellValue,T defaultValue){
        返回可选的。ofNullable(cellValue)。orElse(defaultValue);
}
 
私有布尔值isDelayed(Map <String,String> entry){
        如果 (entry.get("Delayed") == null) { 返回 false; }
        返回(entry.get(“延迟”)。equalsIgnoreCase(“是”));
}
 
私有持续时间delayDurationOf(M​​ap <String,String> entry){
    if (entry.get("延迟时间") == null) { 返回持续时间。;}
    返回 Duration.parse("PT" + entry.get("延迟时间"));
}
@DataTableType
public PastFlight mapRowToPastFlight(Map<String, String> entry) {
    return new PastFlight(
            optional(entry.get("Flight Number"),"FT-101"),
            LocalDate.parse(optional(entry.get("Date"), "2020-10-01")),
            FlightStatus.valueOf(optional(entry.get("Status"), "COMPLETED")),
            isDelayed(entry),
            delayDurationOf(entry));
}
 
private <T> T optional(T cellValue, T defaultValue) {
        return Optional.ofNullable(cellValue).orElse(defaultValue);
}
 
private Boolean isDelayed(Map<String, String> entry) {
        if (entry.get("Delayed") == null) { return false; }
        return (entry.get("Delayed").equalsIgnoreCase("Yes"));
}
 
private Duration delayDurationOf(Map<String, String> entry) {
    if (entry.get("Delayed By") == null) { return Duration.ZERO; }
    return Duration.parse("PT" + entry.get("Delayed By"));
}

在实际项目中,还有许多其他方法可以做到这一点。例如,一些团队使用 JSON 或 Excel 模板文件来包含默认值,并且仅更新指定的字段桌子。

In real-world projects there are many other ways to do this. For example, some teams use a JSON or Excel template file to contain the default values, and only update the fields specified in the table.

8.5 使用背景和钩子进行设置和拆除

8.5 Setting up and tearing down with backgrounds and hooks

测试场景通常需要系统处于特定状态。例如,数据库中必须存在某些记录,用户必须是特定类型的用户或具有特定权限,或者在场景开始之前必须已经执行了某些操作。

Test scenarios often need the system to be in a particular state. For example, certain records must be present in the database, the user must be a particular type of user or have specific permissions, or perhaps certain operations must have already been performed before the scenario can start.

在自动化任何验收标准之前,您需要确保系统处于正确且众所周知的初始状态。此外,许多自动化验收标准(尤其是端到端的验收标准)需要引用或更新数据库中的数据,或者可能以其他格式准备输入数据。某些测试可能需要初始化和配置其他服务或资源,例如文件系统或远程 Web 服务。为了获得有效的自动化验收标准,您需要能够快速、准确且自动地设置所有这些内容(见图 8.6)。

Before you can automate any acceptance criteria, you need to make sure the system is in a correct and well-known initial state. Also, many automated acceptance criteria—particularly the end-to-end variety—will need to refer to or update data in a database, or maybe prepare input data in some other format. Some tests may need other services or resources, such as filesystems or remote web services, to be initialized and configured. In order to have effective automated acceptance criteria, you need to be able to set up all of these things quickly, precisely, and automatically (see figure 8.6).

图 8.6 自动数据设置是自动验收标准的关键部分。

Figure 8.6 Automated data setup is a key part of automated acceptance criteria.

8.5.1 使用后台步骤

8.5.1 Using background steps

在上一章中我们看到的每个场景之前准备测试环境的简单方法是使用后台步骤,该步骤在每个场景开始之前执行,因此是添加适用于每个场景的上下文或准备信息的好地方。例如,假设您正在开发一项功能,该功能描述了常旅客会员在飞行时如何赚取积分的规则:

One simple way to prepare your test environment before each scenario that we saw in the previous chapter is to use a background step, which is executed before the start of each scenario and thus is a good place to add contextual or preparatory information that is true for each scenario. For example, suppose you were working on a feature that describes rules about how Frequent Flyer members earn points when they fly:

特色:通过航班赚取飞行常客积分
  为了提高顾客忠诚度
  作为航空公司销售经理
  我希望旅客在搭乘我们的航班时能够赚取飞行常客积分
Feature: Earning Frequent Flyer points from flights
  In order to improve customer loyalty
  As an airline sales manager
  I want travelers to earn frequent flyer points when they fly with us

常旅客获得的积分数量取决于旅行距离、所乘舱位等级以及当前状态等因素。不同的航线可获得不同的积分;特定航线的积分数量由营销部门根据自己的标准确定。在所有情况下,银卡常旅客可获得额外 50% 的积分,金卡常旅客可获得双倍积分。

The number of points a frequent flyer earns depends on factors such as the distance they travel, the cabin class they fly in, and their current status. Different routes are worth different numbers of points; the number of points for a given route is determined by the marketing department based on their own criteria. In all cases, Silver Frequent Flyers earn an extra 50% points and Gold Frequent Flyers earn double points.

您可以使用以下后台步骤来捕获航班时刻表以及可能适用于多种情况的其他数据:

You could use the following background step to capture a flight schedule and other data that might hold for several scenarios:

背景  给定以下航班时刻表:
    | 从 | 至 | 舱位 | 积分 |
    | 伦敦 | 纽约 | 经济 | 550 |
    | 伦敦 | 纽约 | 商务 | 800 |
    | 伦敦 | 纽约 | 第一 | 1650 |
    | 纽约 | 洛杉矶 | 经济 | 100 |
    | 纽约 | 洛杉矶 | 商务 | 140 |
    | 纽约 | 洛杉矶 | 第一 | 200 |
  以及以下传单类型乘数:
    | 状态 | 乘数 |
    | 标准 | 1.0 |
    | 银色 | 1.5 |
    | 黄金 | 2.0 |
Background:
  Given the following flight points schedule:
    | From     | To          | Class    | Points |
    | London   | New York    | Economy  | 550    |
    | London   | New York    | Business | 800    |
    | London   | New York    | First    | 1650   |
    | New York | Los Angeles | Economy  | 100    |
    | New York | Los Angeles | Business | 140    |
    | New York | Los Angeles | First    | 200    |
  And the following flyer types multipliers:
    | Status   | Multiplier |
    | Standard | 1.0        |
    | Silver   | 1.5        |
    | Gold     | 2.0        |

然后,您可以继续编写更简洁、更集中的场景,以这些背景步骤为基础。例如,您可以从一个简单的场景开始,该场景说明如何根据航线和舱位赚取积分:

You can then proceed to write more concise and focused scenarios that build on these background steps. For example, you could start with a simple scenario that illustrates how points are earned based on the route and the cabin class:

场景:旅客根据积分计划赚取积分
  鉴于Stacy 是标准飞行常客计划会员
  她乘坐经济舱从伦敦飞往纽约时
  那么她应该获得 550 分
Scenario: Travelers earn points depending on the points schedule
  Given Stacy is a Standard Frequent Flyer member
  When she flies from London to New York in Economy class
  Then she should earn 550 points

该功能中的每个后续场景都将开发和探索业务规则和约束。例如,另一个场景可能建立在第一个场景的基础上,并说明舱位对获得的积分数量的影响:

Each subsequent scenario in the feature would develop and explore business rules and constraints. For instance, another scenario might build on the first one and illustrate the effect of the cabin class on the number of points earned:

情景概述:旅客乘坐更高舱位可赚取更多积分
  鉴于Silvia 是银卡常旅客会员
  她乘坐 <Cabin> 舱位从 <From> 飞往 <To> 时
  那么她应该获得 <Points Earned> 积分
  例如    | 从 | 至 | 客舱 | 所得积分 | 备注 |
    | 伦敦 | 纽约 | 经济舱 | 550 | 国际航段 |
    | 纽约 | 洛杉矶 | 商务舱 | 800 | 国内航段 |
    | 纽约 | 伦敦 | 头等舱 | 1650 | 回程 |
Scenario Outline: Travelers earn more points in higher cabin classes
  Given Silvia is a Silver Frequent Flyer member
  When she flies from <From> to <To> in <Cabin> class
  Then she should earn <Points Earned> points
  Examples:
    | From     | To          | Cabin    | Points Earned | Notes             |
    | London   | New York    | Economy  | 550           | International leg |
    | New York | Los Angeles | Business | 800           | Domestic leg      |
    | New York | London      | First    | 1650          | Return trip       |

第三个例子可以探讨飞行常客身份对所获积分数量的影响:

And a third example could explore the effect of frequent flyer status on the number of points earned:

情景概述:飞行常客身份等级越高,赚取的积分就越多
  鉴于Tracy 是 <Status> 常旅客会员
  她乘坐商务舱从 <From> 飞往 <To> 时
  那么她应该获得 <Points Earned> 积分
  例如    | 从 | 至 | 状态 | 所得积分 |
    | 伦敦 | 纽约 | 标准 | 800 |
    | 纽约 | 洛杉矶 | 银牌 | 210 |
    | 纽约 | 伦敦 | 黄金 | 1600 |
Scenario Outline: Higher frequent flyer status levels earn more points
  Given Tracy is a <Status> Frequent Flyer member
  When she flies from <From> to <To> in Business class
  Then she should earn <Points Earned> points
  Examples:
    | From     | To          | Status   | Points Earned |
    | London   | New York    | Standard | 800           |
    | New York | Los Angeles | Silver   | 210           |
    | New York | London      | Gold     | 1600          |

后台步骤的步骤定义代码的编写方式与任何其他步骤相同;事实上,对于 Cucumber 来说,步骤出现在后台步骤中还是出现在普通场景中并没有区别。例如,以下清单显示了上面显示的步骤之一的步骤定义的实现。

The step definition code for background steps is written in the same way as any other steps; in fact, for Cucumber, it makes no difference whether a step appears in a background step or in an ordinary scenario. For example, the following listing shows an implementation of a step definition for one of the steps shown above.

清单 8.17 用于从 Gherkin 表转换行的 DataTableType 注释

Listing 8.17 The DataTableType annotation used to convert rows from Gherkin tables

@DataTableType
公共 PointsSchedule pointsSchedule ( Map <String,String> 行) {
    返回 PointsSchedule.from(row.get("From"))
                         .to(row.get("至"))
                         .flyingIn(CabinClass.valueOf(row.get("Class")))
                         .earns(Integer.parseInt(row.get("Points")));
}
 
@Given("以下航班时刻表:")
公共无效的FollowingFlightPointsSchedule(列表<PointsSchedule> 
点时间表){
    航班数据库.添加点计划(pointSchedules);
}
@DataTableType
public PointsSchedule pointsSchedule(Map<String, String> row) {
    return PointsSchedule.from(row.get("From"))
                         .to(row.get("To"))
                         .flyingIn(CabinClass.valueOf(row.get("Class")))
                         .earns(Integer.parseInt(row.get("Points")));
}
 
@Given("the following flight points schedule:")
public void theFollowingFlightPointsSchedule(List<PointsSchedule> 
 pointSchedules) {
    flightDatabase.addPointSchedules(pointSchedules);
}

但有时我们需要执行一些我们不想向业务读者公开的任务。也许这些任务太过技术性而无法引起人们的兴趣,或者它们依赖于实现,我们不想在我们的场景中公开它们。这时我们就需要使用另一种技术,我们将在下一节中了解它:钩子。

But sometimes we need to perform tasks that we don’t want to expose to the business reader. Maybe they are too technical to be of interest, or maybe they are implementation dependent, and we don’t want to expose them in our scenarios. That’s where we need to use another technique, which we will learn about in the next section: hooks.

8.5.2 使用钩子

8.5.2 Using hooks

不是所有设置步骤都应在后台步骤中进行。后台步骤旨在面向业务并可供业务阅读:它们应仅包含业务读者关心的信息。

Not all setup steps should go in a background step. Background steps are designed to be business facing and business readable: they should only contain information that business readers care about.

但有时我们需要设置业务不太感兴趣的系统的其他方面。例如,我们可能需要执行一些低级的日常活动,如打开浏览器、设置数据库或在测试完成后删除测试数据。我们可以使用钩子来做到这一点。钩子是一种在生命周期的不同点被调用的方法,您可以在其中注入自己的自定义代码来执行任何您需要做的事情,以使您的测试数据和环境保持您想要的状态。

But there are times when we need to set up other aspects of the system that the business isn’t so interested in. For example, we may need to perform some low-level housekeeping activity such as opening a browser, setting up a database, or deleting test data once the test has completed execution. We can do this using hooks. A hook is a method that will get called at different points in your life cycle, where you can inject your own custom code to do whatever you need to do to keep your test data and environment just the way you want it.

所有 BDD 工具都支持某种钩子机制,但具体细节因工具而异。在本节的其余部分,我们将介绍一些使用钩子在 Cucumber 中准备和管理测试环境的实际示例。

All BDD tools support some sort of hook mechanism, though the details will vary from tool to tool. In the rest of this section, we will look at a few practical examples of using hooks to prepare and manage test environments in Cucumber.

在场景发生之前和之后进行干预

Intervening before and after a scenario

钩子最常见的用途可能是在每个场景开始之前进行干预。例如,如果您的应用程序使用数据库,您可以在每次测试之前自动重新初始化数据库模式,并可能使用合理的预定义参考数据集填充它。这样,任何测试都不会无意中通过向数据库添加意外数据来破坏另一个测试。它还确保每个场景都独立于其他场景,并且不依赖于事先执行的另一个场景。

The most common use of hooks is likely intervening before each scenario starts. For example, if your application uses a database, you could automatically reinitialize the database schema before each test, possibly populating it with a sensible predefined set of reference data. This way, no test can inadvertently break another test by adding unexpected data into the database. It also ensures that each scenario is independent of the others and doesn’t rely on another scenario to have been executed beforehand.

钩子最常见的用途之一是在执行场景之前使用它们来设置测试环境。确保每个场景都处理好自己的测试环境和数据是一个好习惯,这样就可以在必要时单独运行。钩子方法可以安全地避开业务读者的视线,但仍在每个场景开始时执行,是执行此操作的好地方。在 Cucumber 中,您可以使用 io.cucumber.java.Before 注释,如以下清单所示。

One of the most common situations for hooks is to use them to set up a test environment before a scenario is executed. It’s a good habit to make sure each scenario takes care of its own test environment and data so that it can be run in isolation if necessary. And hook methods, safely out of view of the business reader, but still executed at the start of each scenario, are a great place to do this. In Cucumber, you use the io.cucumber.java.Before annotation, as seen in the following listing.

清单 8.18 在测试生命周期的不同点运行的钩子方法

Listing 8.18 Hook methods running at different points in the test life cycle

@之前                                     
公共无效prepareStaticData(){
    FlightDatabase.instance()。setupAirports();
    飞行数据库.实例()。初始化默认飞行计划();
}
@Before                                     
public void prepareStaticData() {
    FlightDatabase.instance().setupAirports();
    FlightDatabase.instance().initialiseDefaultFlightPlans();
}

Before标签告诉Cucumber在执行每个场景之前运行此方法。

The Before tag tells Cucumber to run this method before each scenario is executed.

当使用 TypeScript 中的 Cucumber 时,代码看起来非常相似,如下所示。

When working with Cucumber in TypeScript, the code will look very similar, as follows.

清单 8.19 TypeScript 中的钩子方法

Listing 8.19 Hook methods in TypeScript

Before(同步函数 () {                                            
    等待FlightDatabase.instance().setupAirports();                  
    等待FlightDatabase.instance()。initialiseDefaultFlightPlans();
});
Before(async function () {                                           
    await FlightDatabase.instance().setupAirports();                 
    await FlightDatabase.instance().initialiseDefaultFlightPlans();
});

就像在 Java 中一样,Before 标签告诉 Cucumber 在执行每个场景之前运行此函数。

Just like in Java, the Before tag tells Cucumber to run this function before each scenario is executed.

所有 Cucumber 步骤定义函数都支持返回 Promises 的异步函数。

All Cucumber step definition functions support asynchronous functions that return Promises.

所有 Cucumber 步骤定义函数 (GivenWhenThen以及钩子BeforeAfterBeforeAll, 和AfterAll)支持返回异步函数Promises。这里我们使用async/await语法来简化异步调用的实现,设置 FlightDatabase

All Cucumber step definition functions (Given, When, Then as well the hooks Before, After, BeforeAll, and AfterAll) support asynchronous functions that return Promises. Here we use the async/await syntax to simplify the implementation of asynchronous calls setting up the FlightDatabase.

获取有关场景的更多信息

Getting more information about the scenario

有时您需要有关正在运行的场景的更多信息。例如,您可能想知道场景的名称,或者它是通过还是失败,以便进行日志记录或故障排除目的。

Sometimes you need more information about the scenario that you are running. For example, you might want to know the name of the scenario, or whether it passed or failed, for logging or troubleshooting purposes.

清单 8.20 将场景详细信息传递给钩子方法

Listing 8.20 Passing scenario details to the hook method

@后                                
public void logScenarioResult(Scenario scene) {                          
    System.out.println (场景.getName () + ":" + 场景.getStatus());
}
@After                                
public void logScenarioResult(Scenario scenario) {                         
    System.out.println(scenario.getName() + ":" + scenario.getStatus());
}

有关场景的详细信息将包含在场景参数中。

Details about the scenario will be included in the Scenario parameter.

在特定场景之前和之后进行干预

intervening before and after specific scenarios

清单 8.20 中所示的钩子方法适用于测试套件中的每个场景。但有时您需要只针对某些场景执行日常工作。例如,假设您的一些测试是打开浏览器的 Web 测试。您可能希望在钩子中打开浏览器@Before实例,然后在@After钩子中关闭浏览器。但是,如果场景不与 Web UI 交互,您不想不必要地打开和关闭浏览器。您可以使用标签和钩子在 Cucumber 中轻松完成此操作。

The hook methods shown in listing 8.20 run for each and every scenario in a test suite. But sometimes you need to perform housekeeping for certain scenarios only. For example, suppose some of your tests are web tests that open a browser. You might want to open the browser instance in a @Before hook, and then close the browser in an @After hook. However, you don’t want to open and close the browser unnecessarily if the scenario does not interact with the web UI. You can do this easily in Cucumber using tags and hooks.

第一步是添加标签到我们想要运行的场景。例如,您可以@web向需要浏览器运行的场景添加一个标签:

The first step is to add a tag to the scenarios we want to run. For example, you could add a @web tag to the scenarios that require a browser to run:

@web
场景:旅客根据积分计划赚取积分
  鉴于Stacy 是标准飞行常客计划会员
  她乘坐经济舱从伦敦飞往纽约时
  那么应该获得 550 分
@web
Scenario: Travelers earn points depending on the points schedule
  Given Stacy is a Standard Frequent Flyer member
  When she flies from London to New York in Economy class
  Then she should earn 550 points

接下来,添加@Before@After钩子方法为每个场景打开和关闭浏览器(参见清单 8.21 和 8.22)。

Next, you add @Before and @After hook methods to open and close the browser for each scenario (see listings 8.21 and 8.22).

清单 8.21 仅在特定场景下运行的钩子方法

Listing 8.21 Hook methods that only run for specific scenarios

WebDriver 驱动程序;
 
@Before(“@web”)
public void prepareDriver() {        
    驱动程序=新的C​​hromeDriver();
}
 
@After(“@web”)                       
公共无效关闭浏览器(){
    驱动程序.退出();
}
WebDriver driver;
 
@Before("@web")
public void prepareDriver() {       
    driver = new ChromeDriver();
}
 
@After("@web")                      
public void closeBrowser() {
    driver.quit();
}

打开 Chrome 浏览器,用于@web注释的场景

Opens the Chrome browser for @web-annotated scenarios

在 @web 注释的场景后关闭 Chrome 浏览器

Closes the Chrome browser after @web-annotated scenarios

您传递给每个注释的参数描述了标签。此参数称为标签表达式,可以是单个标签(例如@web)或标签的逻辑组合(@web@smoketest@web与不@backend)描述这些钩子方法需要的情况跑步。

The parameter you pass to each annotation describes the tag. This parameter is known as a tag expression and can be a single tag (such as @web) or a logical combination of tags (@web and @smoketest or @web and not @backend) that describe the circumstances when these hook methods need to run.

清单 8.22 TypeScript 中特定于标签的钩子方法

Listing 8.22 Tag-specific hook methods in TypeScript

驱动程序:WebDriver;
 
Before({ tags: '@web' }, function () {          
    驱动程序=新的C​​hromeDriver();
});
 
After({ tags: '@web' }, async function () {     
    等待驱动程序.关闭();
});
let driver: WebDriver;
 
Before({ tags: '@web' }, function () {         
    driver = new ChromeDriver();
});
 
After({ tags: '@web' }, async function () {    
    await driver.close();
});

在 CucumberJS 中,标记钩子是使用作为第一个参数传递的对象文字来定义的。

In CucumberJS, tagged hooks are defined using an object literal passed as the first parameter.

请注意,钩子可以是异步的并使用 async/await 语法、返回 Promise 或调用回调。

Note that hooks can be asynchronous and use the async/await syntax, return a Promise, or invoke a callback.

在影片开始和结束时进行干预

intervening at the start and end of a feature

最多场景在开始之前需要一个新的环境。这是一个很好的做法;否则,先前场景的结果可能会干扰测试套件中稍后执行的场景,并导致不可预测的测试失败。

Most scenarios need a fresh environment before they start. And this is a good practice; otherwise, the results of previous scenarios can interfere with scenarios executed later in the test suite and lead to unpredictable test failures.

但有时,你只想在测试运行开始时准备一次测试环境或测试环境的某些部分。例如,如果你的测试需要启动 Docker 实例(我们将在本章后面介绍如何执行此操作),你可能只想启动该实例一次,然后在每个场景中重复使用它。在 TypeScript 中,这很容易:你可以使用BeforeAllAfterAll钩子,它们分别在任何场景执行之前执行,并在所有场景完成后执行,如下所示。

But sometimes you want to prepare a test environment, or some parts of a test environment, only once, at the start of the test run. For example, if your tests need to start a Docker instance (we will see how to do this later in the chapter), you may want to start the instance only once and then reuse it for each scenario. In TypeScript, it’s very easy: you can use the BeforeAll and AfterAll hooks, which are executed, respectively, before any scenario is executed, and at the end once they have all finished, as follows.

清单 8.23 在 TypeScript 中执行任何场景之前运行钩子

Listing 8.23 Running hooks before any scenario is executed in TypeScript

dbContainer:StartedTestContainer 
BeforeAll({ 超时: 10 * 1000 }, async () => {                       
    dbContainer = await new DatabaseContainer(dbConfig).start();      
});
 
AfterAll (异步 () => {
    等待dbContainer.stop();                                         
});
let dbContainer: StartedTestContainer;
 
BeforeAll({ timeout: 10 * 1000 }, async () => {                      
    dbContainer = await new DatabaseContainer(dbConfig).start();     
});
 
AfterAll(async () => {
    await dbContainer.stop();                                        
});

除了标签之外,Cucumber JS hooks 支持的另一个选项是超时,以毫秒为单位。在这里,我们覆盖了 5 秒的默认超时,并给我们的 DatabaseContainer 最多 10 秒的启动时间。如果我们的测试需要在不如我们编写它们的开发机器那么强大的环境中运行,那么增加默认超时会很有用。

Apart from tags, another option Cucumber JS hooks support is timeout, expressed in milliseconds. Here, we overwrite the default timeout of 5 seconds and give our DatabaseContainer up to 10 seconds to start up. Increasing the default timeout can be useful if our test needs to run in environments not as powerful as the development machine we write them on.

实例化一个DatabaseContainer,异步启动它,并存储对已启动容器的引用。

Instantiate a DatabaseContainer, start it asynchronously, and store the reference to the started container.

停止容器。

Stop the container.

在 Cucumber with Java 中,@BeforeAll@AfterAll钩子注释(在 Cucumber 7 中引入了 Java 版本)起着同样的作用。例如,在下面的清单中,我们使用一个简单的静态变量来确保我们的数据库只初始化一次。

In Cucumber with Java, the @BeforeAll and @AfterAll hook annotations (introduced in Java in Cucumber 7) play the same role. For example, in the following listing we use a simple static variable to ensure that our database is only initialized once.

清单 8.24 在 Java 中执行任何场景之前准备数据

Listing 8.24 Preparing data before any scenario is executed in Java

@BeforeAll
公共无效prepareStaticData(){
    测试数据库.实例() .setupAirports();
    测试数据库.实例() .initialiseDefaultFlightPlans();
}
@BeforeAll
public void prepareStaticData() {
    TestDatabase.instance().setupAirports();
    TestDatabase.instance().initialiseDefaultFlightPlans();
}

这种方法在很多情况下都很好,但根据代码的实现方式,它可能不是线程安全的。这意味着如果你并行运行测试,你可能会遇到麻烦,因为代码可能会在多个线程中同时执行线。

This approach is fine in many cases, but depending on how your code is implemented, it may not be thread safe. This means that if you are running your tests in parallel, you may run into trouble, as the code could be executed simultaneously in more than one thread.

使用 Cucumber 事件监听器

Using Cucumber EventListeners

其他在测试生命周期的不同阶段进行干预的一种方法就是利用 Cucumber EventListeners。这需要编写一些额外的代码,但与使用静态变量相比,这种方法更简洁、更模块化。

Another way to intervene at different points in the test life cycle is to leverage the Cucumber EventListeners. This involves writing a bit of extra code but can result in a cleaner and more modular approach than using static variables.

编写 Cucumber EventListener 很简单。首先,你需要编写一个实现该io.cucumber.plugin.EventListener接口的类。这个接口只有一个方法,setEventPublisher()。在此方法中,您可以为感兴趣的事件注册处理程序,在本例中,即测试运行开始和结束时的处理程序。下面是 Cucumber EventListener 的一个示例。

Writing a Cucumber EventListener is straightforward. First, you need to write a class that implements the io.cucumber.plugin.EventListener interface. This interface has a single method, setEventPublisher(). Inside this method, you register handlers for the events you are interested in, in this case, when a test run starts and finishes. An example of a Cucumber EventListener follows.

清单 8.25 使用静态标志在 Java 中执行任何场景之前准备数据

Listing 8.25 Preparing data before any scenario is executed in Java using a static flag

公共类 DatabaseServerHandler 实现 EventListener {
    @Override
    公共无效设置事件发布者(事件发布者事件发布者){
        eventPublisher.registerHandlerFor(TestRunStarted.class,        
                事件 -> {
                    测试数据库.实例() .startServer();
                    测试数据库.实例() .initialiseDefaultFlightPlans();
                });
 
        eventPublisher.registerHandlerFor(TestRunFinished.class,       
                事件 -> {
                    测试数据库.实例() .stopServer();
                });
    }
}
public class DatabaseServerHandler implements EventListener {
    @Override
    public void setEventPublisher(EventPublisher eventPublisher) {
        eventPublisher.registerHandlerFor(TestRunStarted.class,       
                event -> {
                    TestDatabase.instance().startServer();
                    TestDatabase.instance().initialiseDefaultFlightPlans();
                });
 
        eventPublisher.registerHandlerFor(TestRunFinished.class,      
                event -> {
                    TestDatabase.instance().stopServer();
                });
    }
}

在测试套件启动之前运行此代码

Runs this code before the test suite starts

测试套件完成后运行此代码

Runs this code after the test suite finishes

下一步是配置您的@CucumberOptions测试套件使用您的侦听器类。您只需在注释的插件属性中包含侦听器类的路径即可在你的测试中跑步者类,例如,在下面的清单中。

The next step is to configure your test suite to use your listener class. You do this by simply including the path to your listener class in the plug-in attribute of the @CucumberOptions annotation in your test runner class, for example, in the following listing.

清单 8.26 配置运行器类以使用 Cucumber EventListener

Listing 8.26 Configuring a runner class to use a Cucumber EventListener

@CucumberOptions(
        插件 = {“com.manning.bddinaction.plugins.DatabaseServerHandler”},
        功能 = “类路径:功能”
公共类AcceptanceTestSuite {}
@CucumberOptions(
        plugin = {"com.manning.bddinaction.plugins.DatabaseServerHandler"}, 
        features = "classpath:features"
)
public class AcceptanceTestSuite {}

包含你的 EventListener 的完整路径并将其添加到插件属性中。

Include the full path to your EventListener and add it to the plug-in attribute.

8.6 使用钩子准备测试环境

8.6 Preparing your test environments using hooks

本节中,我们将了解如何使用挂钩方法和开源工具来设置和准备测试数据和测试环境。

In this section, we look at how we can use hook methods with open source tools to set up and prepare test data and test environments.

8.6.1 使用内存数据库

8.6.1 Using in-memory databases

一个钩子最流行的用法是在执行一个场景或一组场景之前准备一个测试数据库。许多团队使用内存数据库(如 H2 或 HSQLDB)进行测试数据。内存中数据库可以快速创建并随后销毁,因此每个场景都可以使用一组新的测试数据。

A popular use of hooks is to prepare a test database before a scenario or a set of scenarios is executed. Many teams use in-memory databases such as H2 or HSQLDB for their test data. In-memory databases are fast to create and to destroy afterward, so each scenario can use a fresh set of test data.

8.7 使用虚拟测试环境

8.7 Using virtual test environments

内存数据库既快速又方便,但也有一些缺点。某些应用程序可能会使用仅在生产数据库中可用的功能,例如专有数据类型或存储过程。许多应用程序还依赖于其他组件,例如消息队列、NoSQL 数据库或分布式缓存。因此,许多团队更喜欢在更像生产的环境中运行集成和端到端测试。

In-memory databases are fast and convenient but do have some drawbacks. Some applications may use features that are only available in the production database, such as proprietary data types or stored procedures. Many applications also rely on other components, such as messaging queues, NoSQL databases, or distributed caches. For this reason, many teams prefer to run integration and end-to-end tests in a more production-like environment.

传统上,团队通常使用专用的测试环境来运行集成和端到端测试。但是,这种方法存在一些问题。以这种方式在本地运行测试很困难,并且必须部署到特定环境中进行测试会减慢反馈周期并造成瓶颈。此外,这些环境可能不稳定;它们也可能用于手动或探索性测试,并且测试可能会因部署或更新而意外中断。

Traditionally, teams often use a dedicated test environment to run integration and end-to-end tests. However, there are some issues with this approach. Running tests this way locally is difficult, and having to deploy into a specific environment to test slows down feedback cycles and creates bottlenecks. In addition, these environments can be unstable; they might also be used for manual or exploratory testing, and testing can be unexpectedly interrupted by deployments or updates.

另一种方法是使用虚拟化在本地机器上运行您自己的类似生产的环境。用于此目的的最常见工具是 Docker。

An alternative approach is to use virtualization to run your own production-like environment on your local machine. And the most common tool used for this purpose is Docker.

Docker 是一款功能强大的工具,可让您将应用程序或服务及其所需的所有依赖项捆绑在一起,然后在另一台机器上启动它。从实际角度来说,这意味着您可以打包需要测试的应用程序或服务,并在需要运行测试的任何时间和地点使用它们。我们将这些称为捆绑的应用程序或服务容器

Docker is a powerful tool that lets you bundle up an application or service, along with all the dependencies it needs, and spin it up on another machine. In practical terms, this means you can package an application or service that you need to test and use whenever and wherever you need to run your tests. We call these bundled applications or services containers.

例如,您可能希望启动一个运行 Postgres 或 MongoDB 数据库的容器来测试数据库持久性(见图 8.7)。或者您可能需要与其他服务(如用于流处理的 Apache Kafka 或 Elasticsearch 搜索引擎)集成。您甚至可以使用名为 Docker Compose 的工具启动一组定制服务。

For example, you might want to start up a container running a Postgres or MongoDB database to test your database persistence (see figure 8.7). Or you might need to integrate with other services such as Apache Kafka for stream processing or an Elasticsearch search engine. You can even start a bespoke set of services using a tool called Docker Compose.

图 8.7 使用 Docker 容器运行数据库实例的简单测试架构

Figure 8.7 A simple test architecture using a Docker container to run a database instance

有多种方法可以将 Docker 用作构建和测试过程的一部分。如果您使用的是 Java,最简单的方法之一就是使用一个名为TestContainers( https://www.testcontainers.org ) 的开源库。

There are many approaches to using Docker as part of your build and testing process. If you are using Java, one of the easiest is to use an open source library called TestContainers (https://www.testcontainers.org).

8.7.1 使用 TestContainers 管理测试的 Docker 容器

8.7.1 Using TestContainers to manage Docker containers for your tests

TestContainers是一个开源库,可让您轻松使用测试需要运行的数据库或其他服务。它在底层使用 Docker(请参阅https://www.docker.com/get-started),因此它可以启动任何可以捆绑到 Docker 容器中的应用程序或服务,但它对许多常见服务提供开箱即用的支持,包括关系和非关系数据库、ElasticSearch、Kafka 和 RabbitMQ。对于更多定制服务,您可以与 Docker Compose 集成或通过提供自己的 Docker 配置文件来创建自定义映像。

TestContainers is an open source library that makes it easy to use databases or other services that your tests need to run. It uses Docker under the hood (see https://www.docker.com/get-started), so it can start any application or service that can be bundled into a Docker container, but it has out-of-the-box support for many common services, including relational and nonrelational databases, ElasticSearch, Kafka, and RabbitMQ. For more tailored services you can integrate with Docker Compose or create a custom image by providing your own Docker configuration file.

配置和使用 的方法有很多种TestContainer。它与 JUnit 和 Spock 等单元测试框架集成良好,是加快传统集成测试速度的好方法。在本节的其余部分,我们将介绍一些可用于 Cucumber 测试套件的更有用的方法。

There are many ways to configure and use TestContainer. It integrates well with unit testing frameworks such as JUnit and Spock and is a great way to speed up conventional integration tests. In the rest of this section, we will look at a few of the more useful approaches that you can use for a Cucumber test suite.

TestContainers 依赖项

The TestContainers Dependencies

TestContainers需要将相应的依赖项添加到 Maven POM 文件或 Gradle 构建脚本中。这包括主testcontainers库,以及您要使用的特定容器的任何依赖项。例如,以下代码显示了使用TestContainersPostgreSQL的:

Before you use TestContainers you need to add the corresponding dependencies to your Maven POM file or Gradle build script. This includes the main testcontainers library, and any dependencies for the specific containers you want to use. For example, the following code shows the Gradle dependencies for a project using TestContainers and PostgreSQL:

testCompile“org.testcontainers:testcontainers:1.15.0-rc2”
testCompile "org.testcontainers:postgresql:1.15.0-rc2"
testCompile "org.testcontainers:testcontainers:1.15.0-rc2"
testCompile "org.testcontainers:postgresql:1.15.0-rc2"

启动 TestContainers 实例

Starting a TestContainers instance

添加这些依赖项后,您就可以开始在测试中使用 Docker 了。使用 Docker 实例就像实例化一个新对象TestContainers一样简单TestContainer. 每个支持的数据库或应用程序都有自己的类,例如PostgreSQLContainerMongoDBContainer, 或者KafkaContainer. 您还可以使用GenericContainer如果您需要使用任意 Docker 实例。例如,要创建并启动新的 PostgreSQL 数据库实例,您可以运行以下清单中的代码。

With these dependencies added, you can start using Docker in your tests. Using a Docker instance with TestContainers is as simple as instantiating a new TestContainer object. Each supported database or application has its own class, such as PostgreSQLContainer, MongoDBContainer, or KafkaContainer. You can also use the GenericContainer class if you need to use an arbitrary Docker instance. For instance, to create and start up a new PostgreSQL database instance, you could run the code in the following listing.

清单 8.27 使用以下命令启动 Docker 实例TestContainers

Listing 8.27 Starting a Docker instance with TestContainers

PostgreSQLContainer 容器 = 新 PostgreSQLContainer (“postgres:13.0”)    
        .withDatabaseName(“集成测试数据库”)                     
        .withUsername(“sa”)                                                 
        .withPassword(“sa”);                                                
 
容器.启动();                                                          
 
String jdbcUrl = container.getJdbcUrl();                                    
PostgreSQLContainer container = new PostgreSQLContainer("postgres:13.0")   
        .withDatabaseName("integration-tests-database")                    
        .withUsername("sa")                                                
        .withPassword("sa");                                               
 
container.start();                                                         
 
String jdbcUrl = container.getJdbcUrl();                                   

Docker 实例的名称和版本

The name and version of the Docker instance

Docker 实例的配置选项

Configuration options for the Docker instance

start() 方法启动容器,并在必要时下载镜像。

The start() method starts the container, downloading the image if necessary.

您可以使用 getJdbcUrl() 方法查找数据库的 JDBC URL。

You can use the getJdbcUrl() method to find the JDBC URL for the database.

通常,您会希望在多个测试中使用一个容器,因此将其封装在自己的类中是有意义的,如下面的清单所示。

Generally, you will want to use a container in more than one test, so it makes sense to encapsulate it in its own class, as shown in the following listing.

清单 8.28 将 封装到TestContainer类中

Listing 8.28 Encapsulating a TestContainer in a class

公共类测试数据库{
    私有静态 PostgreSQLContainer容器
            =新的PostgreSQL容器(“postgres:11.1”)
                    .withDatabaseName(“集成测试数据库”)
                    .withUsername(“sa”)
                    复制代码
 
    公共静态 PostgreSQLContainer getInstance() {
        容器.启动();
        返回容器;
    }
}
public class TestDatabase {
    private static PostgreSQLContainer container
            = new PostgreSQLContainer("postgres:11.1")
                    .withDatabaseName("integration-tests-db")
                    .withUsername("sa")
                    .withPassword("sa");
 
    public static PostgreSQLContainer getInstance() {
        container.start();
        return container;
    }
}

此代码将创建一个数据库实例,供所有测试共享。有时这样做是出于性能原因,但启动TestContainers实例通常非常快,因此完全可以为每个测试使用一个新实例。

This code will create a single database instance that will be shared across every test. This is sometimes done for performance reasons, but spinning up a TestContainers instance is generally so quick that it is perfectly feasible to use a new instance for each test.

@Before在 Cucumber 中,你可以通过在钩子中创建新实例来实现这一点或者,如果你想让容器逻辑与胶水代码分离,你可以修改清单 8.28 中的类,使其线程安全。一个简单的方法是使用ThreadLocal为每个容器维护一个单独的实例线程,如下。

In Cucumber you could do this by creating a new instance in a @Before hook. Alternatively, if you want to keep the container logic separate from your glue code, you could modify the class in listing 8.28 to make it thread safe. A simple way to do this is to use the ThreadLocal class to maintain a separate instance of the container for each thread, as follows.

清单 8.29 使用线程安全TestContainers实例

Listing 8.29 Using thread-safe TestContainers instances

公共类测试数据库{
    私有静态 ThreadLocal<PostgreSQLContainer> 容器
            = ThreadLocal.withInitial            ()->新 PostgreSQLContainer(“postgres:11.1”)
                    .withDatabaseName(“集成测试数据库”)
                    .withUsername(“sa”)
                    .withPassword(“sa”)
    (英文):
 
    公共静态 PostgreSQLContainer getInstance() {
        容器.get().start();
        返回容器.get();
    }
}
public class TestDatabase {
    private static ThreadLocal<PostgreSQLContainer> container
            = ThreadLocal.withInitial(
            () -> new PostgreSQLContainer("postgres:11.1")
                    .withDatabaseName("integration-tests-db")
                    .withUsername("sa")
                    .withPassword("sa")
    );
 
    public static PostgreSQLContainer getInstance() {
        container.get().start();
        return container.get();
    }
}

将 TestContainers 与 Cucumber 和 SpringBoot 集成

Integrating TestContainers with Cucumber and SpringBoot

那里有很多方法可以将这些 Docker 实例与您的测试代码集成,但最具挑战性的方面之一通常是将测试服务器的动态生成的地址注入到您的应用程序代码中,以便它指向正确的数据库和服务。

There are many ways to integrate these Docker instances with your test code, but one of the most challenging aspects is generally to inject the dynamically generated address of the test server into your application code so that it points to the correct database and services.

如果你使用 SpringBoot,一个有用的技巧是使用@DynamicPropertySource注释,它允许你以编程方式覆盖 Spring 配置属性。在清单 8.30 中,你可以看到如何使用此批注动态注入实例化数据库的连接详细信息的示例经过 TestContainers(要了解如何在 TypeScript 中执行此操作,请查看 GitHub repo。)

One useful trick if you are using SpringBoot is to use the @DynamicPropertySource annotation, which lets you override Spring configuration properties programmatically. In listing 8.30 you can see an example of how this annotation can be used to dynamically inject the connection details for the database instantiated by TestContainers. (To see how to do it in TypeScript, check out the GitHub repo.)

清单 8.30TestContainers与 Cucumber 和 SpringBoot 一起使用

Listing 8.30 Using TestContainers with Cucumber and SpringBoot

注释:
@ContextConfiguration
公共类 EarningPointsStepDefinitions {
 
    您可以使用 @DynamicPropertySource 来更改属性值。
    静态 void 属性(DynamicPropertyRegistry 注册表){
        注册表.添加(“spring.datasource.url”, 
                     测试数据库.getInstance()::getJdbcUrl);
        注册表.add(“spring.datasource.用户名”,
                     测试数据库.getInstance()::getUsername);
        注册表.添加(“spring.datasource.密码”,
                     测试数据库.getInstance()::获取密码);
    }
    
    @Autowired
    点计划存储库点计划存储库;
    
    @Given("以下航班时刻表:")
    公共 void setupFlightPointsSchedule(List <PointsSchedule> pointsSchedules){
        pointsScheduleRepository.deleteAll();
        pointsSchedules.forEach(
                时间表 -> pointsScheduleRepository.保存(时间表)
        (英文):
    }
@SpringBootTest
@ContextConfiguration
public class EarningPointsStepDefinitions {
 
    @DynamicPropertySource
    static void properties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", 
                     TestDatabase.getInstance()::getJdbcUrl);
        registry.add("spring.datasource.username",
                     TestDatabase.getInstance()::getUsername);
        registry.add("spring.datasource.password",
                     TestDatabase.getInstance()::getPassword);
    }
    
    @Autowired
    PointsScheduleRepository pointsScheduleRepository;
    
    @Given("the following flight points schedule:")
    public void setupFlightPointsSchedule(List<PointsSchedule> pointsSchedules) {
        pointsScheduleRepository.deleteAll();
        pointsSchedules.forEach(
                schedule -> pointsScheduleRepository.save(schedule)
        );
    }

概括

Summary

  • 为了运行我们的可执行规范,我们用我们选择的语言编写自动测试代码,该代码将在 Cucumber 场景的每个步骤中执行(在本章中,我们重点关注 Java 和 JavaScript/TypeScript)。

  • To run our executable specifications, we write automated test code in the language of our choice that will be executed for each step of your Cucumber scenarios (in this chapter, we focused on Java and JavaScript/TypeScript).

  • 钩子和后台步骤可用于准备您的测试环境或随后进行整理。

  • Hooks and background steps can be used to prepare your test environment or tidy up afterward.

  • 许多团队使用 Docker 来创建和管理虚拟测试环境。该TestContainers库是一种在测试代码中管理 Docker 实例的便捷方法。

  • Many teams use Docker to create and manage virtual test environments. The TestContainers library is a convenient way to manage Docker instances from within your test code.

在下一章中,你将学习如何编写自动化验收测试来测试网络界面。

In the next chapter, you’ll learn how to write automated acceptance tests that exercise a web interface.


1  Gherkin 语言不是我们编写可执行规范的唯一方式,但它是最常用的方式之一,因此在接下来的几章中,我们将重点关注这种格式的自动化场景的示例。

1  The Gherkin language is not the only way we can write executable specifications, but it is one of the most frequently used, so we will focus our examples on automating scenarios in this format in the next few chapters.

2  高度实验性的初创应用程序可能属于这一类。如果企业需要获得市场反馈来发现他们真正需要的功能,那么很难将非常详细的场景形式化。

2  Highly experimental start-up applications might fall into this category. If the business needs to get market feedback to discover what features they really need, it will be hard to formalize scenarios with very much detail.

3  在撰写本文时,Java 是迄今为止测试自动化中最常用的语言,而 JavaScript 则是增长最快的语言。在本书中,我们使用 TypeScript,它提供了所有 JavaScript 功能以及 TypeScript 类型系统。这种类型的系统将使您更容易避免常见的编程错误,并使您的编辑器或 IDE 更容易理解程序的结构,从而更好地支持您的工作。

3  Java is by far the most commonly used language in test automation at the time of writing, whereas JavaScript is the fastest growing. In this book we’re using TypeScript, which offers all of the JavaScript features, as well as the TypeScript type system. This type of system will make it easier for you to avoid common programming mistakes and easier for your editor or IDE to understand the structure of your programs and therefore better support your work.

9 编写可靠的自动化验收测试

9 Writing solid automated acceptance tests

本章封面

This chapter covers

  • 编写高质量自动化测试的重要性
  • The importance of writing high-quality automated tests
  • 实施可靠且可持续测试的模式
  • Patterns for implementing reliable and sustainable tests

在接下来的几章中,我们将研究如何将前面几章讨论的自动化场景转变为针对不同类型的应用程序和技术的完全自动化验收测试。在深入研究技术细节之前,我们需要先了解一下大局。我们需要考虑如何确保无论使用哪种技术或技术组合,我们的测试质量都能保持较高水平。

In the next few chapters, we’ll look at turning the automated scenarios we discussed in the previous chapters into fully automated acceptance tests for different types of applications and technologies. Before we dive into the technical details, we need to look at the bigger picture. We need to consider how to ensure that, no matter what technology or combination of technologies we use, the quality of our tests remains high.

良好的验收测试能够清晰地传达其意图,并提供有关应用程序当前状态的有意义的反馈。它可靠且易于维护,因此测试提供的价值超过了维护它的成本。

A good acceptance test communicates its intent clearly and provides meaningful feedback about the current state of the application. It is reliable and easy to maintain so that the value provided by the test outweighs the cost of maintaining it.

但是,如果自动验收测试设计不当,就会增加维护开销,当添加新功能时,更新和修复的成本要高于它们对项目的价值贡献。大量测试自动化计划正是由于这个原因而失败或变得无效,因此设计良好的验收测试非常重要。在本章中,我们将介绍一些技术和模式,它们可以帮助您编写有意义、可靠且可维护的自动验收测试。让我们首先讨论一下什么是工业强度的验收测试。

But when automated acceptance tests are poorly designed, they add to the maintenance overhead, costing more to update and fix when new features are added than they contribute in value to the project. A large number of test automation initiatives fail or become ineffective for this very reason, so it’s important to design your acceptance tests well. In this chapter, we’ll look at a number of techniques and patterns that can help you write automated acceptance tests that are meaningful, reliable, and maintainable. Let’s start by discussing what makes an industrial-strength acceptance test.

9.1 编写工业强度的验收测试

9.1 Writing industrial-strength acceptance tests

它是编写一个简单的自动化测试并不难。互联网上有大量书籍和资源可以告诉你如何为各种技术编写自动化测试,我们将在本书后面介绍其中的一些。但更难的是确保你的测试经得起时间的考验。你的测试会在几个月后继续提供有用的反馈吗?随着代码库的增长,它们会变得更难维护吗?它们会变得脆弱不堪,以至于当它们出现故障时团队不愿意去修复它们吗?许多团队花费了大量精力构建大型复杂的测试套件,最终却放弃了它们,因为他们发现维护和更新它们的成本超过了它们提供的反馈的价值。

It’s not hard to write a simple automated test. There are plenty of books and resources on the internet that can show you how to write automated tests for all sorts of technologies, and we’ll look at a few of these later on in this book. But what’s harder is ensuring that your tests will stand the test of time. Will your tests continue to provide useful feedback several months down the road? Will they become harder to maintain as the code base grows? Will they become brittle and fragile, discouraging the team from fixing them when they break? Many teams spend a great deal of effort in building large and complex test suites, only to abandon them because they find that the cost of maintaining and updating them outweighs the value of the feedback they provide.

编写良好的自动验收测试套件中的测试应为通过或待定。失败的自动验收测试对开发团队来说应该是危险信号,需要立即引起注意。但是,当测试由于零星的技术问题而频繁失败时,团队将对其作为反馈机制失去信心,并且在测试失败时缺乏修复它们的动力(见图 9.1)。这会导致恶性循环,构建中总会有一些失败的测试。发生这种情况时,自动验收标准不再发挥其主要作用,即提供有关项目当前健康状况的反馈。这是您想要避免的。

The tests in a well-written automated acceptance test suite should be either passing or pending. A failing automated acceptance test should be a red flag for the development team that demands immediate attention. But when tests fail too often due to sporadic technical problems, the team will lose confidence in them as a feedback mechanism and be less motivated to fix them when they break (see figure 9.1). This leads to a vicious circle, where there are always a few broken tests in the build. When this happens, the automated acceptance criteria no longer perform their primary role of providing feedback about the project’s current health. This is what you want to avoid.

图 9.1 验收测试失败过于频繁会导致自满。

Figure 9.1 Acceptance tests that fail too often can lead to complacency.

这是一个重要的问题,值得提前付出一些努力,以确保您投入到自动化验收标准上的时间在整个项目期间及之后都能充分发挥作用。自动化测试就像任何其他类型的代码一样 - 如果您希望它们健壮且易于维护,您需要在设计上花点功夫,并以清晰易懂的方式编写它们。编写良好的自动化验收测试需要与编写良好的生产代码相同的软件工程技能和准则,以及相同的工艺水平。最好的自动化验收测试往往是测试人员和开发人员共同努力的结果。

It’s an important problem, and it’s worth putting in some effort up front to ensure that the time you invest automating your acceptance criteria yields its full fruits over the duration of the project and beyond. Automated tests are like any other kind of code—if you want them to be robust and easy to maintain, you need to put a little effort into their design and write them in a way that’s clear and easy to understand. Writing good, automated acceptance tests requires the same software engineering skills and disciplines, and the same level of craftsmanship, as well-written production code. The best automated acceptance tests tend to be the result of a collaborative effort between testers and developers.

不幸的是,自动化测试通常得不到应有的重视。这个问题的一部分是历史原因——多年来,测试自动化一直是测试脚本的代名词。团队不愿意在编写脚本上投入太多精力,因为他们可以编写生产代码。许多更传统的测试工具确实使用了脚本语言,这些语言对标准软件工程实践的支持很差,或者根本不鼓励这些实践,例如重构以避免代码重复、编写可重用组件以及以干净易读的方式编写代码。

Unfortunately, automated tests often don’t receive the attention they need. Part of this problem is historical—for many years, test automation has been synonymous with test scripts. Teams have been reluctant to put too much effort into writing scripts when they could be writing production code. Many of the more traditional testing tools do indeed use scripting languages that have poor support for, or simply don’t encourage, standard software engineering practices, such as refactoring to avoid code duplication, writing reusable components, and writing code in a clean and readable manner.

如果团队应用一些简单的原则,许多这些问题都可以避免。良好的自动化验收测试需要遵守一些关键规则:

Many of these issues can be avoided if teams apply a few simple principles. A good, automated acceptance test needs to respect a few key rules:

  • 它应该清晰地传达信息。自动验收测试首先是沟通工具。措辞和语义很重要;测试需要清楚地解释它们所展示的业务行为。他们需要向利益相关者、业务分析师、测试人员和其他团队成员解释应用程序旨在支持哪些业务任务,并说明它是如何工作的。理想情况下,这些测试将持续到项目开发阶段之后,并继续帮助维护或照常营业团队 (BAU),负责应用程序部署到生产环境中的团队;在许多组织中,这个团队与开发项目的团队不同)了解项目需求以及它们是如何实现的。

  • It should communicate clearly. Automated acceptance tests are first and foremost communication tools. Wording and semantics are important; the tests need to clearly explain the business behavior they’re demonstrating. They need to explain to stakeholders, business analysts, testers, and other team members what business tasks the application is intended to support and illustrate how it does. Ideally, these tests will outlive the development phase of the project and go on to help the maintenance or business as usual team (BAU, the team that takes care of applications once they’re deployed into production; in many organizations, this is a different team than the one developing the project) understand the project requirements and how they’ve been implemented.

  • 它应该提供有意义的反馈。如果测试失败,开发人员应该能够理解底层需求试图实现什么,以及它如何与应用程序交互来实现这一点。

  • It should provide meaningful feedback. If a test fails, a developer should be able to understand what the underlying requirement is trying to achieve and how it’s interacting with the application to do so.

  • 它应该是可靠的。为了从测试中获得价值,团队需要能够信任测试结果。当(且仅当)应用程序满足底层业务需求时,测试才应该通过,否则应该失败。如果测试确实因技术原因而中断,问题应该易于隔离和修复。虽然这听起来很明显,但可能需要谨慎和纪律来确保更复杂的测试遵守此规则。

  • It should be reliable. To get value out of the tests, the team needs to be able to trust the test results. A test should pass if (and only if) the application satisfies the underlying business requirement and should fail if it doesn’t. If a test does break for technical reasons, the problem should be easy to isolate and simple to fix. Although this sounds obvious, it can require care and discipline to ensure that the more complex tests respect this rule.

  • 它应该易于维护。如果测试在应用程序更新或修改时经常中断,或者需要不断维护才能保持最新状态,那么很快就会成为开发团队的负担。如果这种情况发生得太频繁,开发人员通常会停止更新测试,让它们处于中断状态。当这种情况发生时,测试结果中的任何有用反馈都会丢失,投入到构建测试套件的时间也会浪费掉。

  • It should be easy to maintain. A test that breaks too often when the application is updated or modified, or that requires constant maintenance to keep up to date, rapidly becomes a liability for the development team. If this happens too frequently, the developers will often simply cease updating the tests and leave them in a broken state. When this happens, any useful feedback from the test results is lost, and the time invested in building up the test suite is wasted.

在本章的其余部分,我们将讨论如何编写高质量的测试,这些测试既遵守这些规则,又能提供有价值的反馈,并且健壮且易于维持。

In the rest of this chapter, we’ll look at how you can write high-quality tests that respect these rules and that provide valuable feedback and are robust and easy to maintain.

9.2 使用角色和已知实体

9.2 Using personas and known entities

在第 7 章中,我们了解了如何使用流畅的构建器以干净易读的方式描述应用程序或系统中对象的初始状态。描述场景系统状态的另一种非常有用的技术是使用角色已知实体。在用户体验 (UX) 领域),角色是虚构的人物,用于代表将使用系统的不同类型的人。角色通常带有非常详细的描述,包括从虚构人物的姓名和电子邮件地址到他们的兴趣、爱好和工作习惯的所有内容。图 9.2 显示了 Flying High 客户的角色。

In chapter 7 we saw how fluent builders can be used to describe the initial state of an application or of objects in the system in a clean and readable manner. Another very useful technique when describing the state of a system for a scenario is to use personas or known entities. In the domain of user experience (UX), personas are fictional characters that are meant to represent the different types of people who will be using the system. A persona usually comes with a very detailed description, including everything from the fictional person’s name and email address to their interests, hobbies, and work habits. Figure 9.2 shows a persona for a Flying High customer.

图 9.2 角色是一个虚构的人物,用来代表系统一类用户。

Figure 9.2 A persona is a fictional character meant to represent a category of user of the system.

9.2.1 在场景中使用角色

9.2.1 Working with persona in your scenarios

什么时候在为场景设置测试数据时,角色会以一个众所周知的名称收集一组精确的数据。因此,当您在以下场景中提到 Jane 时,团队中的每个人都会知道您在谈论谁。以下是 Jane 的一个场景的示例:

When it comes to setting up test data for your scenarios, a persona assembles a set of precise data under a well-known name. So, when you refer to Jane in the following scenario, everyone on the team will know who you’re talking about. Here’s an example of one of Jane’s scenarios:

场景:在线注册新的飞行常客账户
  鉴于Jane 不是飞行常客计划会员
  Jane 注册新账户
时  然后她应该收到一封确认电子邮件
  应该获得 500 分
Scenario: Registering online for a new Frequent Flyer account
  Given Jane is not a Frequent Flyer member
  When Jane registers for a new account
  Then she should be sent a confirmation email
  And she should receive 500 bonus points

每当场景中提到 Jane 时,测试代码就会查找相应的角色详细信息。例如,在第二步(“当 Jane 注册新帐户时”)中,测试代码可以引用 Jane 的所有详细信息来完成注册表单,而无需它们出现在场景本身中。

Whenever Jane is referred to in the scenario, the test code will look up the corresponding persona details. For example, in the second step (“When Jane registers for a new account”), the test code can refer to all of Jane’s details to complete the registration form, without them needing to appear in the scenario itself.

在您的代码中实现基于角色的测试数据的方法有很多,最好的方法取决于您的具体应用程序。有时,与每个角色相关的数据存储在外部文件中,例如 JSON 文件或属性文件。在其他应用程序中,默认角色数据存储在测试数据库中,并在每次测试开始时初始化。在下一节中,我们将介绍一种简单有效的策略,用于将测试数据存储在类似 JSON 的配置中文件。

There are many ways to implement persona-based test data in your code, and the best way will depend on your specific application. Sometimes the data related to each persona is stored in external files, such as a JSON file or properties file. In other applications default persona data is stored in a test database and initialized at the start of each test. In the next section we’ll look at one simple and effective strategy for storing test data in JSON-like configuration files.

9.2.2 在 HOCON 中存储人物角色数据

9.2.2 Storing persona data in HOCON

将角色数据存储在外部文件中的非常方便的方法是使用 Typesafeconfighttps://github.com/lightbend/config)。此开源库允许您以 HOCON 格式存储数据(https://github.com/lightbend/config/blob/master/HOCON.md)。HOCON是 JSON 的超集,被许多开源工具用作配置文件。HOCON 比纯 JSON 更灵活、更易读,是存储测试数据和表示场景中角色的绝佳方式。以下清单显示了 HOCON 文件的示例,其中我们描述了两个具有不同个人信息的角色 Jane 和 Terry。

One very convenient way to store persona data in external files is to use the Typesafe config library (https://github.com/lightbend/config). This open source library allows you to store data in the HOCON format (https://github.com/lightbend/config/blob/master/HOCON.md). HOCON is a superset of JSON used by many open source tools for configuration files. More flexible and readable than plain JSON, HOCON is a great way to store test data and represent the persona that figure in your scenarios. An example of a HOCON file is shown in the following listing, where we describe two persona, Jane and Terry, who have different personal details.

清单 9.1 Typesafe 使用的 HOCON 格式存储的测试数据config

Listing 9.1 Test data stored in the HOCON format used by Typesafe config

简:{
  名字: Jane
  姓氏:Smith
  电子邮件:“jane@acme.com”
  街道: 10 Partridge Street
  城市: 丹德农
  州:维多利亚州
  邮政编码:3175
  国家:澳大利亚
  电话:“0123456789”
  出生日期:1981-08-29
}
特里:{
  名字:Terry
  姓氏:旅行者
  电子邮件:“terry@dummy-email.com”
  街道: 主街 100 号
  城市: 都柏林
  国家:爱尔兰
}
Jane: {
  firstName: Jane
  lastName: Smith
  email: "jane@acme.com"
  street: 10 Partridge Street
  city: Dandenong
  state: Victoria
  postCode: 3175
  country: Australia
  telephone: "0123456789"
  dateOfBirth: 1981-08-29
}
Terry: {
  firstName: Terry
  lastName: Traveler
  email: "terry@dummy-email.com"
  street: 100 Main Street
  city: Dublin
  country: Ireland
}

我们之前看到的场景的第一步的步骤定义代码引用角色名称来检索相应的详细信息:

The step definition code for the first step of the scenario we saw earlier refers to the persona name to retrieve the corresponding details:

旅行者 travelerDetails;
 
@Given("{} 不是飞行常客会员")
公共无效notAFrequentFlyer(字符串名称){
    旅行者详细信息 = TravelerPersonas.findByName(姓名);
}
Traveler travelerDetails;
 
@Given("{} is not a Frequent Flyer member")
public void notAFrequentFlyer(String name) {
    travelerDetails = TravelerPersonas.findByName(name);
}

在幕后,TravelerPersonas班级使用 Typesafe Config API从存储在 src/test/resources/testdata 文件夹中的名为 travelers.conf 的测试文件中读取人物数据,如下面的清单所示。

And behind the scenes, the TravelerPersonas class uses the Typesafe Config API to read the persona data from a test file called travelers.conf stored in the src/test/ resources/testdata folder, as seen in the following listing.

清单 9.2 从 HOCON 文件读取测试数据

Listing 9.2 Reading test data from an HOCON file

导入 com.typesafe.config.Config;
导入 com.typesafe.config.ConfigFactory;
 
公共类旅行者人物角色{
 
    私有静态配置旅行者 
        = ConfigFactory.load(“测试数据/旅行者”);
 
    公共静态旅行者 findByName(字符串名称){
        配置 travelerDetails = travelers.getConfig(name);
        返回新的旅行者(
                travelerDetails.getString(“firstName”),
                travelerDetails.getString(“lastName”),
                travelerDetails.getString(“电子邮件”),
                travelerDetails.getString(“街道”),
                travelerDetails.getString(“城市”),
                travelerDetails.getString(“州”),
                travelerDetails.getString(“邮政编码”),
                travelerDetails.getString(“国家”),
                travelerDetails.getString(“电话”),
                旅行者详细信息.getString(“出生日期”)
        (英文):
    }
}
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
 
public class TravelerPersonas {
 
    private static Config travelers 
        = ConfigFactory.load("testdata/travelers");
 
    public static Traveler findByName(String name) {
        Config travelerDetails = travelers.getConfig(name);
        return new Traveler(
                travelerDetails.getString("firstName"),
                travelerDetails.getString("lastName"),
                travelerDetails.getString("email"),
                travelerDetails.getString("street"),
                travelerDetails.getString("city"),
                travelerDetails.getString("state"),
                travelerDetails.getString("postCode"),
                travelerDetails.getString("country"),
                travelerDetails.getString("telephone"),
                travelerDetails.getString("dateOfBirth")
        );
    }
}

类似的方法也适用于其他类型的测试数据。团队定义已知实体——处于已知状态的域对象。例如,一个用于传输银行对账单文件的银行应用程序可能会定义一些标准文件类型,然后只指定具有不同值的字段。给定设想。

A similar approach can be used with other types of test data. The team defines known entities—domain objects in a well-known state. For example, a banking application working on transferring bank statement files might define a few standard file types and then only specify the fields that have different values for a given scenario.

9.3 抽象层

9.3 Layers of abstraction

自动化验收测试需要稳定可靠。当应用程序发生小变化时,您不需要更新数百个验收测试来保持测试套件的运行。维护自动化验收测试绝不会成为您接受变化的能力的障碍。

Automated acceptance tests need to be stable and reliable. When small changes happen in the application, you shouldn’t need to update hundreds of acceptance tests just to keep the test suite running. Maintaining your automated acceptance tests should never be a hindrance to your ability to embrace change.

然而,当许多团队开始自动化验收标准时,就会发生这种情况。通常,一个常用网页上的一个小变化就会破坏大量测试,开发人员需要单独修复每个测试,然后测试套件才能重新运行。这是吃力不讨好、没有成效的工作,对鼓励团队自动化更多验收测试几乎没有什么帮助。

Yet when many teams start automating their acceptance criteria, this is exactly what happens. Often a small change in a single, commonly used web page will break a large swath of tests, and developers will need to fix each test individually before the test suite can work again. This is thankless, unproductive work that does little to encourage the team to automate more acceptance tests.

不必如此。提高自动化代码质量的一个非常有效的方法是应用软件工程的基本原则,即抽象层。抽象层隐藏函数或对象内的实现细节。这使得高层更清晰、更容易理解,并将它们与隐藏在较低层内的实现细节的变化隔离开来。

It doesn’t have to be this way. One very effective way to improve the quality of your automation code is to apply a basic principle of software engineering, known as layers of abstraction. Layers of abstraction hide implementation details inside a function or object. This makes the high-level layers cleaner and easier to understand and isolates them from changes in the implementation details hidden inside the lower layers.

编写自动验收标准时,使用分层可以帮助您将测试中更不稳定的低级实现细节与更高级别、更稳定的业务规则隔离开来。高级业务规则往往相对稳定,对它们的更改将由业务驱动,而不是技术约束。较低级别的实现细节(例如屏幕布局和字段名称以及低级库的调用方式)往往会更频繁地更改。当更改确实发生在较低实现级别时,对自动验收标准的影响应该很小。经验丰富的 BDD 从业者1通常在测试中使用至少三个抽象层,如图 9.3 所示。

When you write automated acceptance criteria, using layers can help you isolate the more volatile, low-level implementation details of your tests from the higher level, more stable business rules. High-level business rules tend to be relatively stable, and changes to them will be driven by the business rather than by technical constraints. Lower-level implementation details, such as screen layouts and field names, as well as how a low-level library is called, tend to change more frequently. When changes do happen at the lower implementation levels, the affect on the automated acceptance criteria should be minimal. Experienced BDD practitioners1 typically use at least three layers of abstraction in their tests, like the ones illustrated in figure 9.3.

图 9.3 精心编写的自动验收标准分为三个主要层次。

Figure 9.3 Well-written automated acceptance criteria are organized in three main layers.

让我们分别看一下这三个主要层。

Let’s look at each of these three main layers.

9.3.1 业务规则层描述预期结果

9.3.1 The Business Rules layer describes the expected outcomes

业务规则层描述需求以高级业务术语进行测试。如果您使用的是 BDD 工具(例如 Cucumber 或 SpecFlow),业务规则通常会采用功能文件中的场景形式,使用表格或叙述结构,就像我们在本章前面看到的那样。在其他情况下,可以使用其他测试自动化库来自动化业务规则层,例如JUnitTestNG(对于 Java)), NUnit(适用于 .NET),或者许多基于 NodeJS 的 JavaScript 测试库中的任何一个。

The Business Rules layer describes the requirement under test in high-level business terms. If you’re using a BDD tool such as Cucumber, or SpecFlow, the business rule will typically take the form of a scenario in a feature file using either a table or a narrative structure, like the one we saw earlier in the chapter. In other cases, the business rules layer can be automated using other test automation libraries such as JUnit or TestNG (for Java), NUnit (for .NET), or any of the many NodeJS-based testing libraries for JavaScript.

为了更好地了解这些层是如何组合在一起的,让我们来看看之前看到的例子:

To get a better feel for how these layers fit together, let’s work through the example we saw earlier:

场景:在线注册新的飞行常客账户
  鉴于Jane 不是常旅客会员       
  Jane 注册新账户时           
  然后她应该会收到一封确认邮件    
  应该获得 500 分奖励         
Scenario: Registering online for a new Frequent Flyer account
  Given Jane is not a Frequent Flyer member       
  When Jane registers for a new account           
  Then she should be sent a confirmation email    
  And she should receive 500 bonus points         

此规则仅适用于新会员。

This rule only applies to new members.

正在测试的高级操作

The high-level action under test

预期成果

The expected outcomes

正如您在上一章中看到的,这种场景关注的是需求的业务结果,而不太担心系统如何实现这些结果。以这种方式表达的需求只有在业务改变对预期结果的想法(或其理解发生变化)时才需要更改。

As you saw in the previous chapter, this sort of scenario focuses on the business outcomes of a requirement and isn’t too worried about how the system delivers these outcomes. Requirements expressed in this way should only need to change if the business changes its mind (or its understanding evolves) about the expected outcomes.

图 9.4 说明了业务规则层如何与其他层协同工作以实现此场景。现在不必担心代码的细节;我们将在本章后面以及后续章节中讨论此示例中使用的技术。

Figure 9.4 illustrates one example of the way the Business Rules layer works together with the other layers to implement this scenario. Don’t worry about the details of the code just yet; we’ll discuss the technologies used in this example later in the chapter and in the following chapters.

图 9.4 每个自动化层都使用下层的服务来实现一个场景。

Figure 9.4 Each automation layer uses services from the layer beneath to implement a scenario.

我们可以将图 9.4 中的代码与我们之前描述的层联系起来(见图 9.5)。业务规则在功能文件和场景中描述。步骤定义方法(或“粘合代码”)描述每个步骤如何转换为业务任务和操作。这些任务和操作在下一层中表示为可重用的对象或方法。最后,在系统交互层,我们定义如何与应用程序进行实际交互。更准确地说,这是我们指定如何在网页上定位元素、调用哪些 REST 端点等等的地方。

We can relate the code in figure 9.4 back to the layers we described earlier (see figure 9.5). Business rules are described in feature files and scenarios. The step definition methods (or “glue code”) describe how each step translates into business tasks and actions. These tasks and actions are represented in the next layer down, as reusable objects or methods. Finally, in the System Interactions layer, we define how we actually interact with the application. More precisely, this is where we specify how to locate elements on a web page, what REST end points to call, and so forth.

图 9.5 每个自动化层都使用下层的服务来实现一个场景。

Figure 9.5 Each automation layer uses services from the layer beneath to implement a scenario.

在接下来的章节中,我们将更详细地介绍这些层细节。

In the following sections we will walk through each of these layers in more detail.

9.3.2 业务流程层描述用户的旅程

9.3.2 The Business Flow layer describes the user’s journey

如果业务规则描述了业务感兴趣的业务目标和约束,业务流程层是您展示系统如何帮助用户实现这些目标或尊重这些约束的地方。这里的目的不是详细描述系统的实现,而是从业务角度对任务或步骤进行高层次的概述。Jane 需要执行哪些高级操作来注册新帐户?您如何知道她收到了确认电子邮件?您将如何逐步完成应用程序来演示此场景?

If the Business Rules describe business goals and constraints that the business is interested in, the Business Flow layer is where you demonstrate how the system helps users achieve these goals or respect these constraints. The aim here is not to describe the implementation of the system in detail, but to give a high-level overview, in business terms, of the tasks or steps. What high-level actions does Jane need to perform to register for a new account? How will you know that she has received the confirmation email? How would you step through the application to demonstrate this scenario?

业务目标和用户旅程

Business goals and user journeys

层是我们定义和描述用户为实现特定目标而执行的每个业务任务的地方。这些任务通常组合在一起来描述用户在系统中的旅程。场景中的每个步骤都映射到一个或多个这些任务的序列。

This layer is where we define and describe each of the business tasks a user performs to achieve a particular goal. These tasks often come together to describe a user journey through the system. Each step in a scenario maps to a sequence of one or more of these tasks.

例如,我们之前介绍的场景的第二步是“当 Jane 注册新帐户时”。我们可以将此步骤分解为以下活动:

For example, the second step of the scenario we introduced earlier is “When Jane registers for a new account.” We could break this step into the following activities:

  • Jane 选择在飞行常客计划网站上注册一个新账户。

  • Jane chooses to register for a new account on the Frequent Flyer website.

  • 简输入了她的姓名和地址。

  • Jane enters her name and address.

  • Jane 确认条款和条件并提交了申请。

  • Jane confirms the terms and conditions and submits her application.

业务流程层应以可读且业务友好的方式表达这些步骤。例如,与此示例相对应的粘合代码可能如下所示(此代码使用 Screenplay 模式,我们将在第 10 章中更详细地介绍):

The Business Flow layer should express these steps in a readable, business-friendly way. For example, the glue code corresponding to this example might look like this (this code uses the Screenplay pattern, which we will look at in more detail in chapter 10):

@When("{actor} 注册新账户")
公共无效寄存器ForANewAccount(演员theTraveler){
    旅行者.尝试(
            Navigate.toTheFrequentFlyerRegistrationPage(),
            输入注册详细信息.使用(旅行者详细信息),
            确认.条款和条件()
    (英文):
}
@When("{actor} registers for a new account")
public void registersForANewAccount(Actor theTraveler) {
    theTraveler.attemptsTo(
            Navigate.toTheFrequentFlyerRegistrationPage(),
            EnterRegistrationDetails.using(travelerDetails),
            Confirms.termsAndConditions()
    );
}

这些步骤比核心业务要求更有可能发生变化,但只有在应用程序工作流程的某些方面发生变化时才需要更改。例如,也许您需要添加一个步骤,让 Jane 在输入详细信息时提供其他详细信息,或者新法律要求她需要确认电子邮件地址才能完成注册过程。

These steps are more likely to change than the core business requirements, but they’ll only need to be changed if some aspect of the application workflow changes. For example, maybe you’ll need to add a step where Jane needs to provide additional details when she enters her details, or perhaps a new law means she needs to confirm her email address before the registration process can be completed.

业务流程层通常从不直接与应用程序本身交互;它委托给较低级别​​的业务任务和系统交互组件。例如,在所示的代码中,我们不讨论单击任何按钮或链接;我们只是说明旅行者导航到应用程序的注册部分。我们没有列出 Jane 在输入详细信息时需要填写的字段,因为它们与故事的这个级别无关。我们只关注她做什么,以及她是如何做到的之后。

The Business Flow layer typically never interacts directly with the application itself; it delegates to lower-level business tasks and system interaction components. For example, in the code shown, we don’t talk about clicking on any buttons or links; we simply state that the traveler navigates to the registration section of the application. We don’t list the fields that Jane needs to fill in when she enters her details, as they are not relevant at this level of the story. We focus purely on what she does, and the how comes later.

宏观场景

Big-picture scenarios

为了对于较大的应用程序,业务流程步骤可以达到更高的级别。假设我们正在开发一款在线汽车保险应用程序。我们可以用如下场景来表示整体流程:

For larger applications, business-flow steps can be even higher level. Suppose we are working on an online car insurance application. We could represent the overall flow with a scenario like this one:

场景:Jane 在网上申请综合保险
  鉴于Jane 拥有一辆新的丰田普锐斯
  Jane 申请综合汽车保险时
  然后她应该看到一个新的混合动力汽车的报价
  应该会通过电子邮件收到一份报价单
Scenario: Jane applies for comprehensive insurance online
  Given Jane owns a new Toyota Prius
  When Jane applies for comprehensive car insurance
  Then she should be shown a New Hybrid Car quote
  And she should receive a copy of the quote via email

这是一个大局观场景。我们并不担心细节,例如 Jane 住在哪里,或者她过去是否出过事故。我们专注于整体流程和高层次目标——在这种情况下,向客户报价,以便他们进行购买。

This is a big-picture scenario. We aren’t worried about the details, such as where Jane lives, or whether she has had any accidents in the past. We are focusing on the overall flow and high-level goals—in this case getting a quote in front of our customers so they can make a purchase.

当然,申请汽车保险报价是一个相当复杂、多步骤的过程,需要输入大量详细信息。您需要了解驾驶员的年龄和地址等个人信息,以及他们驾驶的汽车类型的详细信息。所有这些详细信息在较低级别的步骤中都是必需的。然而,在这个大局中,重点是整体流程,我们不希望它们在这个高层次上搅乱局面。

Of course, applying for a car insurance quote is a rather complex, multistep process and involves entering a lot of details. You need to know personal details such as the driver’s age and address, as well as details about the type of car they drive. All these details will be required in lower-level steps. However, the focus in this big-picture scenario is the overall flow, and we don’t want them muddying the picture at this high level.

为了跟踪所有这些细节,又不让场景的读者被不必要的噪音淹没,我们可以定义角色对象,使其包含有关驾驶员汽车和个人信息的合理默认值,只更改与特定场景相关的值。使用我们在上一章中看到的技术,第一步的代码可能如下所示:

To keep track of all these details, without overwhelming the reader of the scenario with unnecessary noise, we could define persona objects to contain sensible default values about the driver’s car and personal details, only changing the ones that are relevant for a specific scenario. Using the techniques we saw in the previous chapter, the code for the first step might look like this:

@Given("{actor} 拥有 {newOrUsed} {word} {}")
公共无效 ownsACar(Actor theCustomer,
                     新品或二手新品或二手,
                     制作琴弦,
                     字符串模型){
    司机详细信息 = DriverPersonas.withName(theCustomer.getName())
                                  .withVehicle(Vehicle.thatIs(newOrUsed)
                                                      .ofMake(制作)
                                                      .ofModel(模型));
}
@Given("{actor} owns a {newOrUsed} {word} {}")
public void ownsACar(Actor theCustomer,
                     NewOrUsed newOrUsed,
                     String make,
                     String model) {
    driverDetails = DriverPersonas.withName(theCustomer.getName())
                                  .withVehicle(Vehicle.thatIs(newOrUsed)
                                                      .ofMake(make)
                                                      .ofModel(model));
}

在下一步中,我们将从高层次介绍申请流程,展示主要步骤。当 Dave 询问他的汽车报价时,他需要首先提供有关他的汽车的详细信息(例如,汽车的品牌和型号、购买年份以及停放地点),然后提供有关他自己的详细信息(他住在哪里、他的驾驶记录等)。最后,他需要确认他输入的所有详细信息都是准确的。

In the next step we walk through the application process, but at a high level, showing the big steps. When Dave asks for a quote for his car, he needs to first provide details about his car (e.g., the make and model of the car, the year it was purchased, and where it is parked) and then provide details about himself (where he lives, his driving record, etc.). Finally, he needs to confirm that all the details he entered are accurate.

@When("{actor} 申请综合汽车保险")
公共无效适用于综合汽车保险(演员司机){
    驾驶员.尝试(
            申请汽车保险.申请单车保险(),
            ProvideDetails.from(司机详细信息).aboutTheirCar(),
            ProvideDetails.from(driverDetails).aboutThemselves(),
            符合.应用程序详细信息()
    (英文):
}
@When("{actor} applies for comprehensive car insurance")
public void appliesForComprehensiveCarInsurance(Actor theDriver) {
    theDriver.attemptsTo(
            ApplyForAutomobileInsurance.forASingleCar(),
            ProvideDetails.from(driverDetails).aboutTheirCar(),
            ProvideDetails.from(driverDetails).aboutThemselves(),
            Conforms.theApplicationDetails()
    );
}

请注意所有这些任务的级别有多高。高级任务往往比低级实现细节更稳定。客户总是需要申请汽车保险、选择特定产品并提供适当的详细信息。他们需要输入的有关自己的详细信息可能会随着应用程序的发展而改变,但他们需要提供一些个人信息这一事实不会改变。

Notice how high level all these tasks are. High-level tasks tend to be more stable than low-level implementation details. A customer will always need to apply for automobile insurance, choose a particular product, and provide the appropriate details. What details they need to enter about themselves may change as the application evolves, but the fact that they need to provide some personal information will not.

以这种方式编写的高级任务也易于重用和重新组合以进行其他测试。例如,另一种情况可能涉及购买摩托车保险。这可以使用不同的任务来选择产品,但重用与输入个人信息和确认申请相关的任务:

High-level tasks written this way are also easy to reuse and reassemble for other tests. For example, another scenario might involve purchasing motorbike insurance. This could use different tasks for choosing the product but reuse the tasks related to entering personal details and confirming the application:

    驾驶员.尝试(
            申请汽车保险.申请摩托车保险(),
            ProvideDetails.from(driverDetails).aboutTheirMotercycle(),
            ProvideDetails.from(driverDetails).aboutThemselves(),
            符合.应用程序详细信息()
    (英文):
    theDriver.attemptsTo(
            ApplyForAutomobileInsurance.forAMotorcycle(),
            ProvideDetails.from(driverDetails).aboutTheirMotercycle(),
            ProvideDetails.from(driverDetails).aboutThemselves(),
            Conforms.theApplicationDetails()
    );

只有在每一个任务内部我们才会真正与应用程序交互,所以我们只需要在每个任务的一个地方维护这些交互的发生方式。

It’s only inside each of these tasks that we actually interact with the application, so we only need to maintain how these interactions happen in one place for each task.

这种以演员为中心的编码风格被称为剧本(见第 12 章),这只是团队分层代码的众多方法之一。我们将在接下来的章节中介绍有助于实现这种分层的其他模式。让我们看看这些单独的任务是如何执行的工作现在。

This actor-centric style of coding is known as Screenplay (see chapter 12), and it is just one of many approaches that teams use to layer their code. We will look at other patterns that can help achieve this kind of layering in the coming chapters. Let’s look at how these individual tasks do their jobs now.

9.3.3 业务任务与应用程序或其他任务交互

9.3.3 Business tasks interact with the application or with other tasks

作为我们已经看到,精心设计的测试代码是分层实现的。任何给定的层都包含上一层的步骤的实现。并且此实现是通过指定(或编排)下一层应该做什么来完成的(见图 9.5)。

As we have seen, well-designed test code is implemented in layers. Any given layer contains the implementation of steps in the layer above. And this implementation is done by specifying (or orchestrating) what the layer below should do (see figure 9.5).

这通常意味着业务流程层中的任务被分解为更细粒度的业务任务或与应用程序的直接交互。例如,某些任务可能与网页交互,而其他任务可能调用 REST API 或查询数据库。

What this generally means is that tasks in the Business Flow layer are broken down into more granular business tasks or direct interactions with the application. For example, some tasks may interact with a web page, whereas others may call a REST API or query a database.

在上一节中,我们了解了如何注册飞行常客帐户,旅行者需要导航到注册页面并提供其注册详细信息。每个步骤都隐藏了与应用程序的较低级别的交互。例如,要导航到注册页面,旅行者需要打开浏览器并单击“注册”按钮(见图 10.3)。同样,输入注册详细信息将涉及与应用程序的许多交互,最有可能是通过与其他网页交互。

In the previous section, we saw how, to register for a Frequent Flyer account, a traveler needs to navigate to the registration page and provide their registration details. Each of these steps hides lower-level interactions with the application. For example, to navigate to the registration page, the traveler needs to open a browser and click on a Register button (see figure 10.3). And similarly, entering registration details will involve many interactions with the application, most likely by interacting with other web pages.

并非所有交互都通过网页进行。对于许多场景,部分或所有步骤可以通过 REST API 调用、数据库查询、消息队列或其他非基于 UI 的方法更有效地执行。例如,场景中的最后一步是“她应该收到一封确认电子邮件”。此步骤可以使用模拟 Web 服务器或电子邮件测试服务来实现。(您可以在市场上找到许多电子邮件测试工具,例如 TestMail、MailSlurp 和 MailHog。)同样,最后一步“她应该收到 500 个奖励积分”可以简单地调用 REST 端点来查询旅行者当前的飞行常客帐户平衡。

Not all interactions happen via the web pages. For many scenarios, some or all steps can be performed more effectively via REST API calls, database queries, message queues, or other non-UI-based approaches. For example, one of the last steps in the scenario is “And she should receive a confirmation email.” This step could be implemented using a mock web server or an email testing service. (You can find many email testing tools on the market, such as TestMail, MailSlurp, and MailHog.) Similarly, the last step, “And she should receive 500 bonus points,” could be a simple call to a REST end point to query the traveler’s current Frequent Flyer account balance.

9.3.4 技术层与系统交互

9.3.4 The Technical layer interacts with the system

技术层由以某种方式直接与应用程序交互的组件组成。技术组件通常结合两个元素:一些与应用程序的哪个部分交互的领域或应用程序特定细节,以及执行实际交互的工具或库。

The Technical layer is made up of components that interact directly with the application in some way. Technical components typically combine two elements: some domain or application-specific details about what part of the application to interact with and a tool or library that performs the actual interaction.

例如,以下代码描述了用户如何导航到注册页面:

For example, the following code describes how a user navigates to the registration page:

公共静态可执行到TheFrequentFlyerRegistrationPage(){
    return Task.where("{0} 打开飞行常客注册页面",
            Open.url("https://frequent-flyer.flying-high.com"),           
            点击(MenuBar.REGISTER)                                    
    (英文):
}
public static Performable toTheFrequentFlyerRegistrationPage() {
    return Task.where("{0} opens the Frequent Flyer registration page",
            Open.url("https://frequent-flyer.flying-high.com"),          
            Click.on(MenuBar.REGISTER)                                   
    );
}

打开浏览器并访问特定的 URL。

Open the browser to a specific URL.

点击主菜单中的注册链接。

Click on the Register link in the main menu.

该方法使用技术组件:Open,在特定 URL 上打开浏览器,然后Click,单击页面上的指定元素。这些组件都是 Serenity Screenplay 库的一部分,我们将在本书后面详细介绍,它们在底层使用 Selenium WebDriver 来执行实际的交互。

This method uses technical components: the Open class, to open a browser on a specific URL, and the Click class, which clicks on a specified element on the page. Each of these components, which are part of the Serenity Screenplay library that we will be looking at in more detail later in the book, uses Selenium WebDriver under the hood to perform the actual interactions.

测试代码以参数的形式将特定于应用程序的详细信息传递给这些组件。第一个操作使用应用程序 URL。第二个操作使用 WebDriver 定位器对象(我们将在第 10 章中详细了解这些对象)指定用户应该点击的确切位置,我们将其存储在一个单独的类中:

The test code passes application-specific details to these components in the form of parameters. The first action uses the application URL. The second specifies where exactly the user should click using a WebDriver locator object (we’ll learn much more about these in chapter 10), which we store in a separate class:

公共类菜单栏{
    public static final By REGISTER = By.partialLinkText("立即注册");
}
public class MenuBar {
    public static final By REGISTER = By.partialLinkText("Register Now");
}

将定位器保存在单独的类中(如上图)是我们用来尽量减少更改影响的一种技术。精心编写的技术组件可将其他层与低级更改的影响隔离开来。例如,假设注册页面的设计发生变化,涉及 HTML 结构和字段名称的更改。这样的更改不会修改此要求的业务规则或工作流程,并且这些级别不会受到影响。您需要更新的唯一代码是在封装注册页面的页面对象内。此更新适用于使用此页面的任何场景。

Keeping the locators in a separate class like this is a technique we use to minimize the impact of change. Well-written technical components isolate the other layers from the impact of low-level change. For example, suppose the design of the registration page changes, involving changes to the HTML structure and field names. Such a change would modify neither the business rule nor the workflow for this requirement, and those levels wouldn’t be affected. The only code you’d need to update is within the page object that encapsulates the registration page. This update would work for any scenario that uses this page.

此外,您可以在实现用户界面之前实现业务规则和业务流程层。前几层是与测试人员和业务分析师合作编写的,并作为开发工作的指导方针。只有当用户界面相当稳定时,您才能实现技术组件。在接下来的章节中,我们将更仔细地研究可用于为不同类型的应用程序执行这些交互的工具应用程序。

In addition, you can implement the Business Rules and the Business Flow layers before the user interface has been implemented. The first layers are written in collaboration with the testers and business analysts and act as guidelines for development work. Only when the user interface is reasonably stable do you implement the technical components. In the following chapters we will look more closely at the tools you can use to perform these interactions for different types of applications.

概括

Summary

  • 良好的自动化验收测试应清晰地传达信息,并在失败时提供有意义的反馈。它们还应可靠并在设计时考虑到维护。

  • Good, automated acceptance tests should communicate clearly and provide meaningful feedback when they fail. They should also be reliable and designed with maintenance in mind.

  • 在测试场景中使用角色是一种以更具描述性的方式描述系统状态的好方法。

  • Using persona in your test scenarios is a good way to describe the state of the system in a more descriptive way.

  • 抽象层通过在多个层面上将“什么”与“如何”分离,有助于使自动验收标准更加稳健。

  • Layers of abstraction help make the automated acceptance criteria more robust by separating the what from the how at multiple levels.

在下一章中,你将学习如何编写自动化验收测试来测试网络界面。

In the next chapter, you’ll learn how to write automated acceptance tests that exercise a web interface.


1  例如,请参阅 Gojko Adzik 的“如何实施 UI 测试而不自找麻烦”,http://gojko.net/2010/04/13/how-to-implement-ui-testing-without-shooting-yourself-in-the-foot-2/

1  See, for example, Gojko Adzik, “How to implement UI testing without shooting yourself in the foot,” http://gojko.net/2010/04/13/how-to-implement-ui-testing-without-shooting-yourself-in-the-foot-2/.

10 UI 层的自动化验收标准

10 Automating acceptance criteria for the UI layer

本章封面

This chapter covers

  • 为什么以及何时应该编写自动化 UI 测试
  • Why and when you should write automated UI tests
  • 使用 Java 中的 Selenium WebDriver 等工具来自动化 Web 测试
  • Using tools like Selenium WebDriver in Java to automate web tests
  • 在测试中查找页面元素并与之交互
  • Finding and interacting with page elements in your tests
  • 设计模式以编写更易于维护的 Web 测试
  • Design patterns to write more maintainable web tests

在上一章中,您了解了如何使用分层方法进行自动验收测试,从而使您的测试更清晰、更强大、更易于维护。我们讨论了精心设计的自动验收测试中使用的三大层:业务规则层、业务流程层和技术层。在接下来的几章中,我们将重点介绍可用于实现技术层的方法和工具,从用户界面开始。

In the previous chapter, you learned how using a layered approach to automated acceptance testing helps make your tests clearer, more robust, and more maintainable. We discussed the three broad layers used in well-designed automated acceptance tests: the Business Rules layer, the Business Flow layer, and the Technical layer. In the following few chapters, we’ll focus on approaches and tools that can be used to implement the Technical layer, starting with the user interface.

在本章中,我们将讨论自动化基于 Web 的应用程序 UI 测试的技术。用户通过其用户界面与应用程序交互,在现代 Web 应用程序中,UI 实现在整体用户体验中起着重要作用。自动化 Web 测试的屏幕截图可以为测试人员提供宝贵的帮助,它们也是提供描述应用程序行为方式的图解文档的好方法。

In this chapter we’ll discuss techniques to automate UI tests for web-based applications. Users interact with an application through its user interface, and in modern web applications the UI implementation plays a major role in the overall user experience. The screenshots from automated web tests can be a valuable aid for testers, and they’re also a great way to provide illustrated documentation describing how the application behaves.

我们将从多个角度研究自动化 Web 测试,以及一些用于 Web 测试的流行自动化库:

We’ll look at automated web testing from several perspectives, as well as a few popular automation libraries used for web testing:

  • 自动化 Web 测试在测试用户与 UI 的交互以及说明端到端用户与应用程序的交互方面非常有效。但如果设计不当或用于测试非 UI 测试更适合的内容,Web 测试可能会成为维护负担。

  • Automated web tests are very effective at testing user interactions with the UI, and for illustrating end-to-end user interactions with the application. But if they’re badly designed or used to test things that would be better tested by non-UI tests, web tests can become a maintenance liability.

  • Selenium WebDriver是一个流行的开源库,可以用于以多种语言(如Java、JavaScript、Ruby、C#和Python)编写自动化Web测试,是大量开源工具的基础。

  • Selenium WebDriver is a popular open source library that can be used to write automated web tests in a number of languages, such as Java, JavaScript, Ruby, C#, and Python, and is the basis for a large number of open source tools.

  • WebDriver 并不是唯一可用于 Web 自动化的工具。我们还将了解Cypress,它是 JavaScript 领域中 Selenium WebDriver 的一个有趣且更新的替代品。

  • WebDriver is not the only tool you can use for web automation. We will also learn about Cypress, an interesting and more recent alternative to Selenium WebDriver from the JavaScript space.

与其他类型的测试自动化相比,糟糕的设计可能会给 Web 测试工作带来更多损失。我们将介绍一些设计模式和实践,它们可以帮助确保您的测试稳健、可靠且易于维护。

Possibly more so than other types of test automation, web testing efforts can suffer enormously from poor design. We will look at a number of design patterns and practices that can help make sure your tests are robust, reliable, and easy to maintain.

虽然本章重点介绍 Web 测试,但本文讨论的许多工具和方法也适用于其他类型的用户界面。例如,可以使用本文讨论的工具集(Appium ( http://appium.io/ ))有效地测试移动应用。一个基于 WebDriver 的移动应用程序自动化库,我们讨论的设计模式和最佳实践适用于任何类型的 GUI。

Although we’ll focus on web testing in this chapter, many of the tools and approaches discussed here also apply to other types of user interfaces. For example, mobile apps can be tested effectively using the tool set we’ll discuss here by using Appium (http://appium.io/), a WebDriver-based automation library for mobile apps, and the design patterns and best practices we discuss are applicable for any type of GUI.

要编写有效的自动化 Web 测试,您不仅需要知道如何很好地自动化 Web 测试,还需要知道何时应该以及何时不应该使用 Web 测试来自动化场景。

To write effective automated web tests, you need to know not only how to automate web tests well, but also when you should and shouldn’t automate scenarios with web tests.

10.1 何时以及如何测试 UI?

10.1  When and how should you test the UI?

网页与其他类型的测试相比,测试具有一些显着的优势:

Web tests have some significant advantages over other types of testing:

  • 它们可以很好地重现最终用户的行为。虽然非 UI 测试在描述和记录业务规则方面非常有效,但 UI 测试更接近地模仿实际用户行为。因此,许多利益相关者自然倾向于更加信任它们。

  • They reproduce end-user behavior well. While non-UI tests can be very effective at describing and documenting business rules, UI tests imitate actual user behavior more closely. Many stakeholders have a natural tendency to trust them more for this reason.

  • 它们是向利益相关者演示功能和记录应用程序运行方式的绝佳方式。

  • They’re a great way to demo features to stakeholders and to document how the application works.

  • 某些应用程序行为和逻辑仅发生在 UI 级别,无法通过任何其他方式进行测试。

  • Some application behavior and logic happen only at the UI level and cannot be tested in any other way.

  • UI 测试往往更接近测试人员手动测试的内容,因此它们可以减少手动 UI 测试的需要,这对测试人员来说是一笔不小的开销。

  • UI tests tend to be closer to what a tester would test by hand, and so they can reduce the need for manual UI testing, which represents a significant overhead for testers.

从定义上讲,Web 测试旨在验证 UI 行为。但作为端到端测试,Web 测试也可以成为一种有效的方式来说明和检查系统中的所有组件如何协同工作。作为动态文档,Web 测试通常还可以很好地记录用户如何使用系统来实现特定目标。Web 测试还可以帮助业务分析师、测试人员和利益相关者对自动验收更有信心測試。

A web test, by definition, is designed to verify UI behavior. But web tests, as end-to-end tests, can also be an effective way to illustrate and check how all the components in the system work together. Used as living documentation, a web test also often does a great job of documenting how a user will use the system to achieve a particular goal. Web tests can also help give business analysts, testers, and stakeholders more confidence in the automated acceptance tests.

10.2 UI 测试在您的测试自动化策略中处于什么位置?

10.2  Where does UI testing fit in your test automation strategy?

用户界面测试显然有其用途。但你很少需要使用 UI 测试来测试系统的每个方面,而且这样做通常不是一个好主意。事实上,在一个典型的 BDD 项目中,很大一部分自动化验收测试将作为非 UI 测试来实现(见图 10.1)。

UI tests clearly have their uses. But you rarely need to test every aspect of a system using UI tests, and doing so is generally not a good idea. In fact, in a typical BDD project, a significant proportion of automated acceptance tests will be implemented as non-UI tests (see figure 10.1).

图 10.1 一个典型的 BDD 项目将拥有比 UI 自动化验收测试更多的非 UI 自动化验收测试。

Figure 10.1 A typical BDD project will have many more non-UI automated acceptance tests than UI ones.

这些非 UI 测试可以采用多种形式,如您将在第 11 章中看到的那样,包括传统上被归类为集成测试或单元测试的内容。许多自动化验收标准,特别是与业务规则或计算相关的标准,直接使用应用程序代码或通过 REST API 而不是通过用户界面更有效地完成。非 Web 测试可以比端到端 Web 测试更快、更准确地测试特定的业务规则。在开发开始之前,非 UI 测试也更容易实现自动化。

These non-UI tests can take many forms, as you’ll see in chapter 11, including what would traditionally be classed as integration or unit tests. Many automated acceptance criteria, particularly those related to business rules or calculations, are more effectively done directly using the application code or via a REST API rather than through the user interface. Non-web tests can test specific business rules more quickly and more precisely than an end-to-end web test. Non-UI tests are also much easier to automate before development starts.

10.2.1 哪​​些场景应该以 UI 测试的形式实现?

10.2.1 Which scenarios should be implemented as UI tests?

常常很难知道是否要将验收测试实现为 Web 测试还是非 Web 测试,或者在何处使用多种技术的组合。一种简单的方法是记住,您只需要为以下四件事进行 Web 测试:

It can often be tricky to know whether to implement an acceptance test as a web test or a non-web test, or where to use a combination of techniques. One simple approach is to remember that you only really need a web test for four things:

  • 说明关键用户通过系统的旅程

  • Illustrating key user journeys through the system

  • 说明或检查不同情况下用户界面上显示的信息

  • Illustrating or checking what information is presented in the user interface in different circumstances

  • 显示信息在用户界面中的呈现方式

  • Showing how information is rendered in the user interface

  • 记录并验证特定于屏幕的业务逻辑

  • Documenting and verifying screen-specific business logic

让我们看看每一个这些。

Let’s look at each of these.

10.2.2 说明用户旅程

10.2.2 Illustrating user journeys

许多高级 BDD 场景描述了用户在系统中的旅程。我们称这些场景为旅程场景。例如,旅行者预订航班,或客户完成购买。以下是一个很好的例子:

Many high-level BDD scenarios illustrate user journeys through the system. We call these scenarios journey scenarios. For example, a traveler books a flight, or a client completes a purchase. The following would be a good example:

场景:塔拉预订了从伦敦飞往纽约的航班
  鉴于Tara 是注册的飞行常客会员
  搜索了从伦敦飞往纽约的经济舱单程航班
  她预订第一班航班
时  然后她应该被告知她的预订成功
  并且预订应出现在她的“我的预订”部分
Scenario: Tara books a flight from London to New York
  Given Tara is a registered Frequent Flyer member
  And she has searched for one-way flights from London to New York in Economy
  When she books the first available flight
  Then she should be informed that her booking was successful
  And the booking should appear in her My Booking section

要点 小黄瓜

Bullet point Gherkin

有些人认为“给定...何时...然后”符号对于旅程场景来说有点笨拙和不自然。他们宁愿从用户的角度快速描述发生了什么。

Some folks find the Given ... When ... Then notation a bit clunky and artificial for journey scenarios. They’d rather just rattle off what happened, from the user perspective.

使其更具可读性的一种方法是使用 Gherkin 中的项目符号,如下所示:

One way to make this a bit more readable is the bullet-point notation in Gherkin, which looks like this:

场景:塔拉预订了从伦敦飞往纽约的航班
  * Tara 是注册的飞行常客计划会员
  * 她搜索从 伦敦 飞往 纽约 的经济舱单程航班
  * 她预订了第一班航班
  * 她应该被告知她的预订成功
  * 该预订将显示在她的“我的预订”部分中
Scenario: Tara books a flight from London to New York
  * Tara is a registered Frequent Flyer member
  * She searches for one-way flights from London to New York in Economy
  * She books the first available flight
  * She should be informed that her booking was successful
  * The booking appears in her My Booking section

在幕后,Cucumber 将这些步骤与 Given ... When ... Then 版本中的步骤相同,因此您使用的风格很大程度上取决于个人偏好或团队惯例。

Behind the scenes, Cucumber treats these steps identically to the ones in the Given ... When ... Then version, so the style you use is very much a question of personal preference or team conventions.

此类场景通常非常适合进行 UI 测试,因为它们可以很好地说明用户如何与系统交互以实现特定的业务目标。但您不需要太多此类场景。您不需要展示系统中所有可能的路径,只需展示更重要的路径即可。更详尽的测试可以留给运行速度更快的测试。

Scenarios like these are often good candidates for UI tests, because they do a great job illustrating how a user interacts with the system to achieve a particular business goal. But you don’t need too many of them. You don’t need to show every possible path through the system, just the more significant ones. More exhaustive testing can be left to faster-running tests.

您还可以通过将 UI 交互与后端交互相结合来简化高级场景。例如,在上一个场景中,假设注册功能已在另一个场景中进行了测试。在这种情况下,我们可以通过 API 调用而不是通过 Web 注册来实现第一步“假设 Tara 是注册的飞行常客会员”界面。

You can also streamline high-level scenarios by mixing UI interactions with backend interactions. For example, in the previous scenario, suppose that the registration feature has already been tested in another scenario. In that case we could implement the first step, “Given Tara is a registered Frequent Flyer member,” with an API call rather than by registering via the web interface.

10.2.3 在用户界面中说明业务逻辑

10.2.3 Illustrating business logic in the user interface

网页测试还可以说明业务规则如何反映在用户界面中。例如,假设在预订航班时,飞行常客会员应该可以选择座位,这是其他客户无法获得的特权。这将是自​​动化 Web 测试的一个很好的候选对象。

Web tests can also illustrate how business rules are reflected in the user interface. For example, suppose that when they book a flight, Frequent Flyer members should be given the option to choose their seat, a privilege not offered to other customers. This would be a good candidate for an automated web test.

一个好的经验法则是问问自己,您是否在说明用户如何与应用程序交互,或者说明独立于用户界面的底层业务逻辑。现在假设,当新的常旅客会员注册时,我们需要检查电子邮件是否不存在。为此,我们可以编写如下场景:

A good rule of thumb is to ask yourself whether you’re illustrating how the user interacts with the application or underlying business logic that’s independent of the user interface. Now suppose that, when a new Frequent Flyer member registers, we need to check that the email doesn’t already exist. To do this we could write a scenario like the following:

规则:不允许重复的用户名
 
  例如:有人试图用已经使用过的电子邮件进行注册
  迈克·史密斯 (Mike Smith) 是现有的飞行常客会员。
  他的妻子 Jenny Smith 没有飞行常客账户
 
    鉴于Mike Smith 是飞行常客计划会员,其详细信息如下:
      | 用户名 | smiths@example.org |
      | 密码 | 正确密码 |
    Jenny 尝试使用用户名“smiths@example.org”进行注册时
    然后她应该看到一条错误消息,其中包含“电子邮件 
已存在,请尝试其他名称”
    她还应该看到“忘记密码?”链接
Rule: Duplicate usernames are not allowed
 
  Example: Someone tries to register with an email that is already used
  Mike Smith is an existing Frequent Flyer member.
  His wife Jenny Smith does not have a Frequent Flyer account
 
    Given Mike Smith is a Frequent Flyer member with the following details:
      | username | smiths@example.org |
      | password | correct-password |
    When Jenny tries to register with a username of "smiths@example.org"
    Then she should be presented with an error message containing "Email 
 exists, please try another name"
    And she should also be presented with a "Forgot your password?" link

这实际上并不是完整的用户旅程;它检查特定屏幕上的特定业务规则。实现此场景的合理方法是通过用户界面。

This is not really a full user journey; it checks a specific business rule on a specific screen. The logical way to implement this scenario would be via the user interface.

但并非所有情况都如此简单。例如,假设您正在测试注册功能,并且其中一项验收标准包括以下约束:

But not all cases are so cut-and-dry. For example, suppose you were testing the registration feature and that one of the acceptance criteria included the following constraints:

  • 用户应该收到指示所输入密码强度的反馈。

  • The user should receive feedback indicating the strength of the password entered.

  • 只应接受强密码。

  • Only strong passwords should be accepted.

第一个验收标准与用户与网页的交互有关,需要说明如何在登录页面上提供此反馈。这将是自​​动化 Web 测试的一个很好的候选对象。

The first acceptance criterion relates to the user’s interaction with the web page and would need to illustrate how this feedback is provided on the login page. This would be a good candidate for an automated web test.

另一方面,第二个标准是确定什么密码是强密码以及应该允许用户输入什么密码。虽然可以通过用户界面反复提交不同的密码来完成此操作,但这样做会很浪费。您在这里真正要检查的是密码强度算法,因此应用程序代码级测试会更合适的。

The second criterion, on the other hand, is about determining what makes a strong password and what passwords users should be allowed to enter. While this can be done through the user interface by repeatedly submitting different passwords, that would be wasteful. What you’re really checking here is the password-strength algorithm, so an application code–level test would be more appropriate.

10.2.4 记录并验证特定于屏幕的业务逻辑

10.2.4 Documenting and verifying screen-specific business logic

有时我们需要针对特定​​屏幕进行更细粒度的 UI 测试,包括对于典型 BDD 场景来说可能过于详细的场景。例如,我们可能想要检查特定页面上的所有字段是否都正确呈现。

Sometimes we need more fine-grained UI testing for specific screens, including scenarios that might be too detailed for a typical BDD scenario. For example, we might want to check that all the fields on a particular page are rendered correctly.

例如,当新的常旅客会员注册时,他们需要提供有效的电子邮件,因此我们需要进行一些检查以确保他们提供的地址格式正确。我们可以编写一个类似以下的场景来表达这一约束:

For example, when a new Frequent Flyer member registers, they need to provide a valid email, so we need to do some checks to make sure that the address they provide is well formed. We could write a scenario along the following lines to express this constraint:

规则:唯一的用户名应该是有效的电子邮件地址
  场景概述:只应接受结构正确的电子邮件
    鉴于Candy 没有飞行常客账户
    她尝试使用用户名“<username>”进行注册时
    然后她应该被告知“电子邮件格式无效”
    例如      | 用户名 |
      | 不是电子邮件 |
      | noteemail.com |
      | candy@#.com |
Rule: The unique username should be a valid email address
  Scenario Outline: Only correctly structured emails should be accepted
    Given Candy does not have a Frequent Flyer account
    When she tries to register with a username of "<username>"
    Then she should be told "Not a valid email format"
    Examples:
      | username     |
      | not-an-email |
      | notemail.com |
      | candy@#.com  |

此场景给出了一些需要检查的无效电子邮件格式的示例。它显示了当用户输入错误的电子邮件地址时如何通知用户。

This scenario gives a few examples of invalid email formats that would need to be checked. It shows how the user is informed when they enter a bad email address.

但显然还有许多其他无效电子邮件地址的例子。我们不想通过用户界面测试所有这些地址——这会导致大量的 Web 测试,从而减慢测试套件的速度,而不会增加那么多额外的信心。

But there are clearly many, many other examples of invalid email addresses. And we wouldn’t want to test them all via the user interface—that would lead to a large number of web tests that would slow down the test suite, without adding that much additional confidence.

更有效的方法是检查这些其他情况,但级别要低一些。在设计良好的应用程序中,我们只需要通过用户界面检查几个示例。一旦我们确定输入无效的电子邮件地址会收到错误消息,我们就可以使用非基于 UI 的测试来测试其他情况。

A more efficient approach would be to check these other cases at a lower level. In a well-designed application, we would only need to check a few examples through the user interface. Once we have established that entering an invalid email address will get you an error message, we could test the other cases with non-UI-based tests.

许多团队使用单独的 Cucumber 场景来处理更详细的场景,例如:

Many teams use separate Cucumber scenarios for more detailed scenarios like this:

场景:电子邮件地址需要格式正确
  鉴于Candy 没有飞行常客账户
  她想注册一个新的飞行常客账户
时  那么以下电子邮件不应被视为有效:
    | 电子邮件 | 拒绝原因 |
    | 不是电子邮件 | 缺少@部分 |
    | 错误.com | 缺少@ |
    | 错误@ | 任务域 |
    | 错误@#.com | 无效字符 |
    | | 不能为空 |
Scenario: Email addresses need to be well formed
  Given Candy does not have a Frequent Flyer account
  When she wants to register a new Frequent Flyer account
  Then the following emails should not be considered valid:
    | Email        | Reason Rejected    |
    | not-an-email | Missing @ section  |
    | wrong.com    | Missing @          |
    | wrong@       | Mission domain     |
    | wrong@#.com  | Invalid characters |
    |              | Cannot be empty    |

此类场景会打开或导航到给定页面,然后检查此页面上的多个相关案例。例如,我们在这里检查每封无效电子邮件是否被拒绝,而无需为每个案例运行单独的 Web 测试。这比上一个场景中描述的方法(我们使用场景大纲)更有效,但需要在步骤定义代码中添加更多逻辑来循环测试用例。(您可以在本章的示例代码中看到如何实现这一点的示例。)

Scenarios like this open or navigate to a given page, and then check a number of related cases on this page. For example, here we check that each invalid email is rejected, without running a separate web test for each case. This is more efficient than the approach described in the previous scenario (where we used a scenario outline) but requires a little more logic in the step definition code to loop over the test cases. (You can see an example of how this can be implemented in the sample code for this chapter.)

但是我们也可以使用较低级别的单元测试库。通常,只有当我们认为这是记录这些规则的最佳方式,并且业务部门非常关心这些规则并希望它们出现在实际中时,我们才会使用 Cucumber文档。

But we could also use a lower-level unit testing library as well. We would typically only use Cucumber if we felt this is the best way to document these rules, and if they are something that the business care enough about to have it appear in the living documentation.

10.2.5 显示信息在用户界面上的呈现方式

10.2.5 Showing how information is rendered in the user interface

有时我们想要检查更详细的 UI 行为;例如,我们可能想要检查特定页面上的所有字段是否正确呈现,或者表单上的哪些字段是必填的。

Sometimes we want to check more detailed UI behavior; for example, we might want to check that all the fields on a particular page are rendered correctly, or which fields on a form are mandatory.

Gherkin 并不总是指定必填字段最方便的格式,但可以做到这一点。例如,以下场景简洁地描述了注册时有关必填字段的规则:

Gherkin isn’t always the most convenient format to specify mandatory fields, but it can be done. For example, the following scenarios succinctly describe rules about mandatory fields when registering:

规则:注册会员需要填写所有必填字段
  场景:注册时必填字段
    鉴于Candy 没有飞行常客账户
    她想注册一个新的飞行常客账户
时    那么注册时必须提供以下信息:
      | 字段 | 缺失时显示错误消息 |
      | 电子邮件 | 请输入您的电子邮件 |
      | 密码 | 请输入您的密码 |
      | firstName | 请输入您的名字 |
      | lastName | 请输入您的姓氏 |
      | 地址 | 请输入您的地址 |
      | 国家 | 请输入有效的国家 |
 
  场景:客户必须同意注册条款和条件         
    鉴于Candy 没有飞行常客账户
    她试图在未同意条款和条件的情况下进行注册时
    然后她应该被告知“请确认条款和条件”
Rule: Registering members need to complete all the mandatory fields
  Scenario: Mandatory fields for registration
    Given Candy does not have a Frequent Flyer account
    When she wants to register a new Frequent Flyer account
    Then the following information should be mandatory to register:
      | Field     | Error Message If Missing     |
      | email     | Please enter your email      |
      | password  | Please enter your password   |
      | firstName | Please enter your first name |
      | lastName  | Please enter your last name  |
      | address   | Please enter your address    |
      | country   | Please enter a valid country |
 
  Scenario: Customers must agree to the registration terms and conditions         
    Given Candy does not have a Frequent Flyer account
    When she tries to register without approving the terms and conditions
    Then she should be told "Please confirm the terms and conditions"

现在我们已经了解了我们可能想要使用用户界面自动化的场景,让我们看看如何做到这一点。我们将从最常用的开源 Web 测试库开始:Selenium WebDriver。

Now that we’ve had a look at the kind of scenarios where we might want to use user interface automation, let’s look at how we can do this. We’ll start with the most commonly used open source web testing library: Selenium WebDriver.

10.2.6 使用 Selenium WebDriver 自动化基于 Web 的验收标准

10.2.6 Automating web-based acceptance criteria using Selenium WebDriver

本节中,我们将介绍如何使用 Selenium WebDriver 实现 Web 测试自动化。Selenium WebDriver 是一个流行的开源 Web 浏览器自动化库,可用于编写有效的自动化 Web 测试。它也是许多高级 Web 测试工具的基础。在本章中,我们将重点介绍如何在 Java 中使用 WebDriver,下一章我们将介绍这些技术如何在 JavaScript 中工作。但我们在这些章节中讨论的原理和技术通常适用于任何基于 WebDriver 的测试。

In this section, we’ll look at automating web tests using Selenium WebDriver. Selenium WebDriver is a popular open source web browser automation library that can be used to write effective, automated web tests. It also forms the basis for many higher-level web-testing tools. In this chapter we will focus on working with WebDriver in Java, and in the next chapter we will look at how these techniques work in JavaScript. But the principles and techniques we’ll discuss in these chapters will be generally applicable to any WebDriver-based testing.

WebDriver 是一款浏览器自动化工具。它允许您编写启动并与真实浏览器交互的测试。这种交互可以包括简单的按钮或链接点击,或更复杂的鼠标操作,例如悬停或拖放。

WebDriver is a browser-automation tool. It lets you write tests that launch and interact with a real browser. This interaction can include simple clicks on buttons or links, or more sophisticated mouse operations such as hovering or dragging and dropping.

WebDriver 可让您通过检查浏览器中页面的状态来检查测试结果。WebDriver 还允许您在测试过程中截取屏幕截图,这些屏幕截图稍后可用作测试报告或实时文档的一部分。

WebDriver lets you check the test outcomes by inspecting the state of the page in the browser. WebDriver also gives you the ability to take screenshots along the way—screenshots that can be used later as part of the test reports or living documentation.

图 10.2 显示了 WebDriver 的高级视图,它支持大量 Web 浏览器,包括 Firefox、Chrome 和 Microsoft Edge。这允许您在不同环境和不同浏览器中测试您的应用程序。

Figure 10.2 shows a high-level view of WebDriver, which supports a large number of web browsers, including Firefox, Chrome, and Microsoft Edge. This allows you to test your application in different environments and with different browsers.

图10.2 WebDriver架构概览

Figure 10.2 Overview of the WebDriver architecture

您可以使用特定于语言的客户端库编写 WebDriver 测试。它支持多种语言,包括 Java、JavaScript、Ruby、C# 和 Python。WebDriver API 在不同语言之间差别不大,因此您通常可以使用自己最熟悉的语言,或者在编写的简易性、可维护性和实时文档方面最有价值的语言。

You write a WebDriver test using a language-specific client library. Many languages are supported, including Java, JavaScript, Ruby, C#, and Python. The WebDriver API varies little from one language to another, so you can generally use the language you’re most comfortable with, or the one that provides the most value for you in terms of ease of writing, maintainability, and living documentation.

WebDriver 客户端库使用 W3C Webdriver 规范 ( https://w3c.github.io/webdriver/ ) 中定义的标准格式来管理客户端与特定于浏览器的驱动程序之间的通信。大多数主流浏览器都有驱动程序,例如 Chrome、Firefox、Internet Explorer 和 Microsoft Edge。

The WebDriver client libraries use a standard format, defined in the W3C Webdriver Specification (https://w3c.github.io/webdriver/), to manage communication between the client and a browser-specific driver. Drivers exist for most of the main browsers, such as Chrome, Firefox, Internet Explorer, and Microsoft Edge.

同样的协议也适用于远程浏览器。您可以使用 Selenium Grid 设置服务器网络,这样您就可以控制真实或虚拟远程机器上的浏览器。这使得通过并行运行测试来扩展测试套件变得更加容易。许多商业服务(例如 SauceLabs 和 BrowserStack)也建议为您管理和维护 Selenium Grid。

This same protocol also works for remote browsers. You can set up a network of servers using Selenium Grid, which allows you to control browsers on real or virtual remote machines. This makes scaling your test suite by running your tests in parallel much easier. Many commercial services, such as SauceLabs and BrowserStack also propose to manage and maintain a Selenium Grid for you.

WebDriver API 功能强大且灵活,但有多个适用于不同平台的开源库可帮助您在 WebDriver 上更高效、更富有表现力地编写 Web 测试,其中包括 Serenity BDD 和 Selenide (Java)、Serenity/JS、Protractor、Webdriver.io (JavaScript)、Watir (Ruby) 和 Geb (用于 Groovy),仅举几例。在本章的大部分内容中,您将了解如何将 WebDriver API 与 Java 和 JavaScript 结合使用,在撰写本文时,它们是最常见的选项。

The WebDriver API is powerful and flexible, but there are several open source libraries for different platforms that can help you build on WebDriver to write web tests more efficiently and more expressively, including Serenity BDD and Selenide (Java), Serenity/JS, Protractor, Webdriver.io (JavaScript), Watir (Ruby), and Geb (for Groovy), just to name a few. For most of this chapter, you’ll see how to use the WebDriver API with Java and JavaScript, which are, at the time of writing, the most commonly seen options.

10.2.7 在 Java 中开始使用 WebDriver

10.2.7 Getting started with WebDriver in Java

让我们从使用 WebDriver 实现 Web 浏览器自动化的一个非常简单的示例开始。您将使用我们在前几章中讨论过的 Flying High Frequent Flyer 网站的简单版本来说明 WebDriver 的功能。

Let’s start with a very simple example of web browser automation with WebDriver. You’ll illustrate WebDriver’s features using a simple version of the Flying High Frequent Flyer website that we’ve discussed in previous chapters.

如果您想继续学习,可以从 GitHub 存储库或 Manning 网站下载网站和示例代码。示例代码存储库包含两个目录:

If you want to follow along, you can download both the website and the sample code from either the GitHub repository or from the Manning website. The sample code repository contains two directories:

  • flying-high-app 目录包含示例网站(参见侧边栏)。

  • The flying-high-app directory contains the sample website (see sidebar).

  • chapter-09 目录(java)中的子目录包含我们将在本章中讨论的示例 WebDriver 代码。

  • The subdirectories in the chapter-09 directory (java) contain the sample WebDriver code we’ll discuss during the chapter.

运行示例网站

Running the sample website

您将使用的常旅客网站是一个简单的独立网站,可在任何 Web 服务器上运行。这是一个基于 JavaScript 的简单 Web 应用程序。您可以将其部署到自己的 Web 服务器,也可以将其作为独立网站运行。一种方法是使用 Node.js,这是一个轻量级 JavaScript 平台,用于构建和运行基于 JavaScript 的服务器端应用程序。您无需了解任何有关 Node.js 的知识即可运行示例网站;只需按照说明操作即可。

The Frequent Flyer website you’ll use is a simple standalone website that will run on any web server. It’s a simple JavaScript-based web application. You can either deploy it to your own web server or run it as a standalone website. One way to do this is to use Node.js, a lightweight JavaScript platform used to build and run JavaScript-based server-side applications. You don’t need to know anything about Node.js to run the sample site; just follow the instructions.

首先,您需要安装 Node.js,您可以从 Node.js 网站 ( http://nodejs.org/ ) 下载。完成此操作后,进入 flying-high-app 目录和服务器子目录并安装项目依赖项:

First, you need to install Node.js, which you can download from the Node.js website (http://nodejs.org/). Once you’ve done this, go into the flying-high-app directory and into the server subdirectory and install the project dependencies:

$ npm 安装
$ npm install

接下来,使用以下命令启动网站:

Next, start the website with the following command:

$ 嵌套开始
$ nest start

要查看正在运行的应用程序,请打开 Web 浏览器并转到 http://localhost:3000。您应该会看到类似于图 10.3 中的页面。

To view the running application, open a web browser and go to http:/ /localhost:3000. You should see a page similar to the one in figure 10.3.

假设您正在测试常旅客网站的登录功能。注册会员需要输入他们的电子邮件地址和密码才能访问他们的帐户详细信息。登录屏幕类似于图 10.3 中的屏幕。

Suppose you’re testing the sign-in feature of your Frequent Flyer website. Registered members need to enter their email address and password to access their account details. The login screen looks something like the one in figure 10.3.

图 10.3 飞行常客会员使用他们的电子邮件地址和密码来识别自己。

Figure 10.3 Flying High Frequent Flyer members identify themselves using their email address and a password.

一旦会员输入了匹配的电子邮件地址和密码,他们就会收到一条友好的消息,欢迎他们进入会员区(见图 10.4)。我们可以使用如下简单场景来表示此要求:

Once a member has entered a matching email and password, they’ll be welcomed to the member’s area with a friendly message (see figure 10.4). We could represent this requirement using a simple scenario like this:

业务需求:认证
 
  已注册的飞行常客会员可以使用其 
电子邮件
  和密码
 
  @web 测试
  示例:Tracy 成功登录飞行常客计划应用程序
    鉴于Tracy 是 Frequency Flyer 的注册会员
    Tracy 使用有效的用户名和密码登录
时    那么她应该被授予访问自己账户的权限
Business Need: Authentication
 
  Registered Frequent Flyer members can access their account using their 
 email
  and password
 
  @webtest
  Example: Tracy successfully logs on to the Frequent Flyer app
    Given Tracy is a registered Frequency Flyer member
    When Tracy logs on with a valid username and password
    Then she should be given access to her account

现在让我们直接深入到步骤定义,看看如何使用 WebDriver 实现这个场景Java。

Now let’s dive straight into the step definitions and see how this scenario could be implemented using WebDriver in Java.

图 10.4 经过身份验证的会员可以预订航班并查看他们的预订和获得的积分。

Figure 10.4 Authenticated members can book flights and see their bookings and earned points.

10.2.8 设置 WebDriver 驱动程序

10.2.8 Setting up a WebDriver driver

在任何 Selenium Web 测试中,我们需要做的第一件事就是启动一个浏览器来运行测试。正如我们之前看到的,Selenium WebDriver 为我们提供了一个标准 API,我们可以使用它来与不同类型的浏览器进行交互。

One of the first things we need to do in any Selenium web test is to spin up a browser to run our tests in. As we saw earlier, Selenium WebDriver gives us a standard API that we can use to interact with different types of browsers.

你要做的第一件事是声明一个新WebDriver实例。该对象实现了 WebDriver 接口,是您与应用程序进行所有交互的起点。每种不同类型的浏览器都有一个单独的实现类。

The first thing you do is declare a new WebDriver instance. This object, which implements the WebDriver interface, is the starting point for all your interactions with the application. There is a separate implementation class for each different type of browser.

例如,以下代码创建一个新WebDriver实例对于 Chrome(它还会打开 Chrome 浏览器的实例进行交互):

For example, the following code creates a new WebDriver instance for Chrome (it also opens an instance of the Chrome browser to interact with):

  WebDriver 驱动程序 = 新的 ChromeDriver();
  WebDriver driver = new ChromeDriver();

如果我们想使用 Firefox,代码将如下所示:

If we wanted to use Firefox, the code would look like this:

   WebDriver 驱动程序 = 新的 FirefoxDriver();
   WebDriver driver = new FirefoxDriver();

此代码需要 WebDriver 驱动程序二进制文件才能运行。每个浏览器都需要一个特定的驱动程序,即知道如何控制和与浏览器交互的二进制可执行文件。一种选择是下载您选择的浏览器的二进制文件(见表 10.1)并将它们放在您的系统路径上。这并不总是最可靠的方法,因为它可能会使您的测试依赖于您的本地环境安装。

This code needs a WebDriver driver binary to work. Each browser needs a specific driver, a binary executable that knows how to control and interact with the browser. One option is to download the binaries for the browsers of your choice (see table 10.1) and place them on your system path. This is not always the most robust approach, as it can make your tests dependent on your local environment installation.

表 10.1 在哪里可以找到各种 WebDriver 驱动程序

Table 10.1 Where to find the various WebDriver drivers

浏览器

Browser

WebDriver 实现

WebDriver implementation

下载网站

Download site

火狐

Firefox

Geckodriver

Geckodriver

https://github.com/mozilla/geckodriver/releases

https://github.com/mozilla/geckodriver/releases

铬合金

Chrome

ChromeDriver

ChromeDriver

https://chromedriver.chromium.org/downloads

https://chromedriver.chromium.org/downloads

互联网浏览器

Internet Explorer

InternetExplorerDriver

InternetExplorerDriver

https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver

https://github.com/SeleniumHQ/selenium/ wiki/InternetExplorerDriver

微软Edge

Microsoft Edge

edgedriver

edgedriver

https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/

https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/

歌剧

Opera

OperaDriver

OperaDriver

https://github.com/operasoftware/operachromiumdriver/releases

https://github.com/operasoftware/operachromiumdriver/releases

Safari

Safari

SafariDriver

SafariDriver

https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari

https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari

另一个选项是使用 WebDriverManager 1库 ( https://github.com/bonigarcia/webdrivermanager ),它会自动为您下载正确的二进制驱动程序。这就是我们将在本章的代码示例中使用的选项。要使用 下载和设置 Chrome 的 WebDriver 二进制文件WebDriverManager,我们将包含以下代码:

Another option is to use the WebDriverManager1 library (https://github.com/bonigarcia/webdrivermanager), which downloads the right binary driver for you automatically. That’s the option we’ll use in our code examples in this chapter. To download and set up the WebDriver binaries for Chrome using WebDriverManager, we would include the following code:

WebDriverManager.chromedriver() 设置;
WebDriverManager.chromedriver().setup();

现在我们已经了解了如何创建一个新的WebDriver实例,让我们看看这一切是如何融入到我们的 Cucumber 中的代码。

Now that we’ve seen how to create a new WebDriver instance, let’s see how all this fits into our Cucumber code.

配置您的浏览器

Configuring your browser

当您创建新WebDriver实例时,您还可以将配置选项传递给浏览器。

When you create a new WebDriver instance, you can also pass configuration options to the browser.

每个浏览器都有自己的配置类。例如,对于 Chrome,您可以使用ChromeOptionshttp://chromedriver.chromium.org/capabilities)类,将其作为参数传递给ChromeDriver类构造函数

Each browser has its own configuration classes. For example, for Chrome, you can use the ChromeOptions (http://chromedriver.chromium.org/capabilities) class, which you pass as a parameter to the ChromeDriver class constructor

ChromeOptions 选项 = 新的 ChromeOptions();
options.addArguments("开始最大化",
                     “无头”
                     “禁用扩展”,
                     “禁用弹出窗口阻止”
                     “禁用信息栏”);
WebDriver 驱动程序 = 新的 ChromeDriver(选项);
ChromeOptions options = new ChromeOptions();
options.addArguments("start-maximized",
                     "headless",
                     "disable-extensions",
                     "disable-popup-blocking",
                     "disable-infobars");
WebDriver driver = new ChromeDriver(options);

10.2.9 将 WebDriver 与 Cucumber 集成

10.2.9 Integrating WebDriver with Cucumber

为了我们的 Cucumber 场景中,我们可以使用@Before钩子定义一个方法,该方法将在每个涉及用户界面测试的场景之前运行(我们可以使用标签识别):

For our Cucumber scenario, we can use a @Before hook to define a method that will be run before each scenario that involves a user interface test (which we can identify using a tag):

   公共类 AuthenticationStepDefinitions {
 
       WebDriver 驱动程序;                                 
 
       @Before(“@webtest”)                               
       公共无效setupWebdriver(){                
           WebDriverManager.chromedriver().setup();      
           驱动程序 = 新 ChromeDriver();                  
       }
 
    @After(“@webtest”)
    公共无效关闭Webdriver(){
        驱动程序.退出();                                   
    }
}
   public class AuthenticationStepDefinitions {
 
       WebDriver driver;                                
 
       @Before("@webtest")                              
       public void setupWebdriver() {                
           WebDriverManager.chromedriver().setup();     
           driver = new ChromeDriver();                 
       }
 
    @After("@webtest")
    public void closeWebdriver() {
        driver.quit();                                  
    }
}

WebDriver 接口允许我们使用 WebDriver 客户端与浏览器进行交互。

The WebDriver interface allows us to interact with the browser using a WebDriver client.

在每个场景之前使用@webtest标签运行此代码。

Run this code before each scenario with the @webtest tag.

使用 WebDriverManager 下载并设置 chromedriver 二进制文件(可选)

Uses WebDriverManager to download and set up the chromedriver binary (optional)

创建一个新的 WebDriver 实例来打开并连接到 Chrome 浏览器

Creates a new WebDriver instance to open and connect to a Chrome browser

每次测试结束时关闭浏览器

Closes the browser at the end of each test

10.2.10 在步骤定义类之间共享 WebDriver 实例

10.2.10 Sharing WebDriver instances between step definition classes

所示的代码示例有点太简单了,不太现实。一般来说,我们需要共享WebDriver实例跨越多个步骤定义类。

The code sample shown is a little too simple to be realistic. In general, we need to share the WebDriver instance across multiple step definition classes.

我们可以通过存储WebDriver实例来实现这一点在静态字段中。但是,如果您想并行运行功能或场景,这将行不通(稍后会详细介绍)。

We could do this by storing the WebDriver instance in a static field. However, this will not work if you ever want to run your features or scenarios in parallel (more on this later).

更好的方法是使用ThreadLocal对象,一个静态字段,在给定线程中创建的所有对象之间共享。由于每个场景都将在自己的线程中运行,因此这种方法将创建一个单独的WebDriver实例。针对每个场景,但仍然让我们分享WebDriver实例在给定场景的所有步骤定义中。

A better approach is to use a ThreadLocal object, a static field that is shared between all the objects created in a given thread. Since each scenario will run in its own thread, this approach will create a separate WebDriver instance for each scenario but still let us share the WebDriver instance among all of the step definitions in a given scenario.

显示了此方法的一个例子这里:

An example of this approach is shown here:

公共类 WebTestSupport {
 
    私有静态 ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
 
    @Before(“@webtest”)
    公共无效setupDriver(){
        WebDriverManager.chromedriver() 设置;
        WebDriver 驱动程序 = 新的 ChromeDriver(选项);
        驱动程序.设置(驱动程序);
    }
 
    公共静态 WebDriver currentDriver() {
        返回 DRIVER.get();
    }
 
    @After(“@webtest”)
    公共无效关闭驱动程序(){
        驱动程序.获取().退出();
    }
}
public class WebTestSupport {
 
    private static ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
 
    @Before("@webtest")
    public void setupDriver() {
        WebDriverManager.chromedriver().setup();
        WebDriver driver = new ChromeDriver(options);
        DRIVER.set(driver);
    }
 
    public static WebDriver currentDriver() {
        return DRIVER.get();
    }
 
    @After("@webtest")
    public void closeDriver() {
        DRIVER.get().quit();
    }
}

10.2.11 与网页交互

10.2.11 Interacting with the web page

所以到目前为止,我们刚刚设置了WebDriver实例并打开浏览器。下一步(“Tracy 使用有效的用户名和密码登录”)是我们真正与浏览器交互的地方,事情会变得更加有趣。在这一步中,我们需要做三件事:

So far, we have just set up WebDriver instance and opened a browser. The next step (“Tracy logs on with a valid username and password”) is where we actually interact with the browser, and where things get a bit more interesting. In this step, we need to do three things:

  • 在右侧页面打开浏览器。

  • Open the browser on the right page.

  • 输入 Tracy 的登录详细信息。

  • Enter Tracy’s login details.

  • 单击登录按钮并进入主页。

  • Click on the Login button and proceed to the home page.

让我们看看如何在 Cucumber 粘合代码中实现这些步骤。

Let’s see how we would implement these steps in our Cucumber glue code.

在这种情况下,我们以 Tracy 的身份登录应用程序(“假设 Tracy 是注册的常旅客会员”)。 Tracy 是我们测试用户的名称,启动服务器时会自动创建。在上一章中,我们了解了如何使用配置文件来跟踪测试用户数据。在这种情况下,我们将选择一种更简单的方法并创建一个枚举来包含测试用户的电子邮件和密码:

In this scenario we log in to the application as Tracy (“Given Tracy is a registered Frequent Flyer member”). Tracy is the name of our test user, who is created automatically when we start the server. In the previous chapter, we saw how to use configuration files to keep track of test user data like this. In this case, we will opt for a simpler approach and create an enum to contain test user emails and passwords:

公共枚举 FrequentFlyer {
    特蕾西(“tracy@flyinghigh.com”,“trac3”);
 
    公共最终字符串电子邮件;
    公共最终字符串密码;
 
    FrequentFlyer(字符串电子邮件,字符串密码){
        这个.电子邮件=电子邮件;
        这个.密码=密码;
    }
}
public enum FrequentFlyer {
    Tracy("tracy@flyinghigh.com","trac3");
 
    public final String email;
    public final String password;
 
    FrequentFlyer(String email, String password) {
        this.email = email;
        this.password = password;
    }
}

然后我们可以将此枚举作为相应步骤定义方法的参数:

We can then use this enum as a parameter to the corresponding step definition method:

飞行常客;
 
@Given("{} 是 Frequency Flyer 的注册会员")
公共无效常旅客会员(常旅客常旅客){
    这个。频繁飞行者=频繁飞行者;
}
FrequentFlyer frequentFlyer;
 
@Given("{} is a registered Frequency Flyer member")
public void frequentFlyerMember(FrequentFlyer frequentFlyer) {
    this.frequentFlyer = frequentFlyer;
}

接下来,我们需要 Tracy 与 Chrome 浏览器进行交互。我们通过使用driver对象来实现这一点再一次;WebDriver班级有多种方法可以调用来操作 Web 浏览器。例如,要打开给定 URL 上的浏览​​器,我们使用get()方法

Next, we need Tracy to interact with the Chrome browser. We do this by using the driver object once again; the WebDriver class has a wide range of methods you can call to manipulate the web browser. For example, to open the browser on a given URL, we use the get() method:

驱动程序.get(“http://localhost:3000”);
driver.get("http://localhost:3000");

findElement()要与页面上的字段或元素进行交互,我们首先需要使用该方法在页面上找到它。此方法允许我们使用一系列不同的策略来定位元素,我们将在下一节中详细介绍。它返回一个WebElement对象,它使我们能够使用许多其他方法来操作和查询页面上的各个元素。

To interact with a field or element on the page, we first need to find it on the page using the findElement() method. This method allows us to locate the element using a range of different strategies, which we will look at in detail in the next section. It returns a WebElement object, which gives us access to a number of other methods we can use to manipulate and query the individual element on the page.

例如,以下代码根据链接中的文本定位应用程序主页上的登录链接:

For example, the following code locates the login link on the application home page, based on the text in the link:

  WebElement loginLink = driver.findElement(By.linkText("登录"));
  WebElement loginLink = driver.findElement(By.linkText("Login"));

一旦我们获得元素,我们就可以使用以下方法click()sendKeys()(分别单击或在元素中输入一些文本)。例如,以下方法将单击我们刚刚找到的链接:

Once we obtain the element, we can use methods such as click() and sendKeys() (to click on or type some text into an element, respectively). For example, the following method will click on the link we just located:

  登录链接.点击();
  loginLink.click();

您可以在该方法中看到所有这些技术的实际应用:

You can see all of these techniques in action in the method:

@When("{} 使用有效的用户名和密码登录")
公共无效logsOnWithAValidUsernameAndPassword()
  WebDriver 驱动程序 = WebTestSupport.currentDriver();
 
  驱动程序.get(“http://localhost:3000”);                                      
  driver.findElement(By.linkText("登录")).click();                         
  驱动程序.findElement(By.id("email"))。发送密钥(frequentFlyer.email);         
  驱动程序.findElement(By.id("密码"))。发送密钥(frequentFlyer.密码);   
  driver.findElement(By.id("login-button")).click();                        
}
@When("{} logs on with a valid username and password")
public void logsOnWithAValidUsernameAndPassword()
  WebDriver driver = WebTestSupport.currentDriver();
 
  driver.get("http://localhost:3000");                                     
  driver.findElement(By.linkText("Login")).click();                        
  driver.findElement(By.id("email")).sendKeys(frequentFlyer.email);        
  driver.findElement(By.id("password")).sendKeys(frequentFlyer.password);  
  driver.findElement(By.id("login-button")).click();                       
}

在 Chrome 中打开应用程序

Opens the application in Chrome

点击登录链接并导航至登录页面

Clicks on the login link and navigate to the login page

输入 Tracy 的用户名和密码

Enters Tracy’s username and password

点击登录按钮

Clicks on the login button

在这种情况下,我们需要做的最后一件事是检查 Tracy 登录后是否被带到了正确的页面。我们可以通过检查菜单栏中的电子邮件地址来做到这一点(见图 10.3)。findElement()方法返回一个WebElement还为我们提供了获取元素信息(例如其文本内容)的方法。此步骤的粘合代码可以编写如下:

The last thing we need to do in this scenario is to check that Tracy gets taken to the right page once she has logged in. We can do this by checking the email address in the menu bar (see figure 10.3). The findElement() method returns a WebElement class and also gives us methods to fetch information about an element, such as its text content. The glue code for this step could be written as follows:

@Then(“他/她应该被授予访问他/她帐户的权限”)
公共无效应该被授予访问帐户的权限(){
    WebDriver 驱动程序 = WebTestSupport.currentDriver();
    字符串 currentUser = driver.findElement(By.id(“当前-
用户”))。getText();
    断言(currentUser)。是否等于(frequentFlyer.email);
}
@Then("he/she should be given access to his/her account")
public void shouldBeGivenAccessToTheAccount() {
    WebDriver driver = WebTestSupport.currentDriver();
    String currentUser = driver.findElement(By.id("current-
 user")).getText();
    assertThat(currentUser).isEqualTo(frequentFlyer.email);
}

现在我们已经快速了解了 WebDriver 的实际应用,让我们更详细地了解一下 WebDriver API 的各种功能细节。

Now that we’ve had a quick overview of WebDriver in action, let’s look at the various WebDriver API features in more detail.

10.2.12 如何定位页面上的元素

10.2.12 How to locate elements on a page

什么时候在编写自动化 Web 测试时,定位页面上的元素是您需要学习的最基本技能之一。在 WebDriver 中,您想要以某种方式检查或操作的任何对象都由类WebElement表示findElement(). 您可以使用该方法在页面上查找 Web 元素。此方法使用流畅的 API 以非常易读的方式识别对象。例如,在图 10.5 中的代码中,您可以通过查找名称attribute设置为 的 HTML 元素来找到电子邮件字段email

When it comes to writing automated web tests, locating elements on a page is one of the most fundamental skills you need to learn. In WebDriver, any object you’d like to inspect or manipulate in some way is represented by the WebElement class. You can find a web element on a page using the findElement() method. This method uses a fluent API to identify objects in a very readable manner. For example, in the code in figure 10.5, you find the email field by looking for an HTML element with the name attribute set to email.

图 10.5 使用 WebDriver API 定位页面上的元素

Figure 10.5 Locating elements on a page using the WebDriver API

获得元素后,您可以根据需要查询或操作它。在前面的代码行中,您可以使用sendKeys()方法click()模拟用户在字段中输入内容。稍后在测试中,使用方法单击登录按钮(由其 id 属性标识)

Once you have the element, you can query or manipulate it as required. In the preceding code line, you use the sendKeys() method to simulate a user typing something into the field. Later in the test, you click on the login button (identified by its id attribute) using the click() method:

驱动程序.findElement(By.id("登录按钮"))。click();
driver.findElement(By.id("login-button")).click();

最后,在测试结束时,检查登录用户的文本内容,可以通过id属性方便地识别

Finally, at the end of the test, you check the text contents of the logged-in user, conveniently identified by an id attribute:

    字符串 currentUser = driver.findElement(By.id(“当前-
用户”))。getText();
    断言(currentUser)。是否等于(frequentFlyer.email);
    String currentUser = driver.findElement(By.id("current-
 user")).getText();
    assertThat(currentUser).isEqualTo(frequentFlyer.email);

此 API 的优点之一是它不仅非常易读,而且非常易于使用。在现代 IDE 中,可以使用自动完成功能列出 WebDriver API 中使用的各种对象和类的可用方法(见图 10.6)。这使得新开发人员可以轻松学习该 API,而经验丰富的开发人员则可以提高其效率。

One of the nice things about this API is that it’s not only very readable, but it’s very easy to use. In modern IDEs, the auto-complete feature can be used to list the available methods for the various objects and classes used in the WebDriver API (see figure 10.6). This makes the API both easy for new developers to learn and very productive for more experienced developers.

图 10.6 现代 IDE 功能(例如自动完成)使 WebDriver API 易于使用。

Figure 10.6 Modern IDE features, such as auto-completion, make the WebDriver API easy to work with.

通过 id 或 name 识别元素

Identifying elements by id or name

上一个例子中我们使用了 HTMLidname属性来标识此页面上的元素。这种策略很常见;它简单方便,并且当页面结构或样式发生变化时,这些属性不太可能发生变化。事实上,确保页面中所有语义上重要的元素都有唯一的 ID 或名称是一个好主意。

In the previous example we used the HTML id and name attributes to identify elements on this page. This strategy is a common one; it is easy and convenient, and these attributes are less likely to change when the structure or style of the page changes. In fact, it’s a good idea to make sure that all semantically significant elements in a page have a unique ID or name.

这个例子相对简单,因为字段和按钮很容易找到。在实际应用中,情况并非总是如此,有些情况下其他策略更方便。幸运的是,WebDriver 提供了许多其他方法来识别 web元素。

This example is relatively straightforward, as the fields and buttons are easy to find. In real-world applications, this isn’t always the case, and there are some situations where other strategies are more convenient. Fortunately, WebDriver provides a number of other ways to identify web elements.

使用数据属性识别元素

Identifying elements using data attributes

尽管虽然id和 nameattribute不太可能在页面布局发生变化时发生变化,但它们仍然与页面的实现紧密相关,尤其是在使用 JavaScript 框架时。id例如,字段通常与 JavaScript 事件处理相关联,而 name 属性通常用于 HTML 表单中。

While the id and name attribute are unlikely to change when the page layout changes, they are still fairly tightly coupled to the implementation of the page, particularly when JavaScript frameworks are used. The id field is often associated with JavaScript event handling, and the name attribute is often used in HTML forms, for example.

如果您可以控制 HTML 源代码,则更独立的策略是使用 HTML 数据属性。HTML 数据属性专门设计用于让您在标准 HTML 元素中存储附加信息,而不会覆盖更常规的 HTML 属性的使用。

If you have control over the HTML source code, a much more independent strategy is to use an HTML data attribute. HTML data attributes are specifically designed to let you store additional information in standard HTML elements, without overriding the usage of more conventional HTML attributes.

例如,data-testid属性是在你的网页中识别页面元素的流行惯例。测试。您可以在以下清单中的 HTML 代码中看到此约定的一些示例。

For example, the data-testid attribute is a popular convention for identifying elements on a page in your tests. You can see some examples of this convention in the HTML code in the following listing.

清单 10.1 使用数据属性来标识字段

Listing 10.1 Using a data attribute to identify a field

<div class="login-container">
    <form [formGroup]="form" (提交)="登录(表单)">
        <mat-form-field appearance="填充">
            <mat-label>电子邮件</mat-label>
            <input id="电子邮件" data-testid="电子邮件">
        </mat-form-field>
        <br>
        <mat-form-field appearance="填充">
            <mat-label>密码</mat-label>
            <input id="密码" data-testid="密码">
        </mat-form-field>
        <br>    
        <button type="submit" data-testid="login”>登录</button>
    </表单>
</div>
<div class="login-container">
    <form [formGroup]="form" (submit)="login(form)">
        <mat-form-field appearance="fill">
            <mat-label>Email</mat-label>
            <input id="email" data-testid="email">
        </mat-form-field>
        <br>
        <mat-form-field appearance="fill">
            <mat-label>Password</mat-label>
            <input id="password" data-testid="password">
        </mat-form-field>
        <br>    
        <button type="submit" data-testid="login”>Login</button>
    </form>
</div>

通过链接文本识别元素

Identifying elements by link text

什么时候编写自动化 Web 测试时,您经常需要点击链接,以导航到另一个页面或触发某些操作。例如,在我们的常旅客网站的欢迎页面上,用户可以选择搜索航班、查看当前预订或查看帐户详细信息(见图 10.7)。

When you write automated web tests, you often need to click on links, either to navigate to another page or to trigger some action. For example, on the welcome page of our Frequent Flyer site, a user can choose to search flights, to view the current bookings, or to view the account details (see figure 10.7).

图 10.7 超链接通常可以通过其包含的文本来识别。

Figure 10.7 Hyperlinks can often be identified by the text they contain.

此类链接可能没有nameiddata属性你可以使用它来识别它们。但你也可以使用次优方案——链接本身的文本。要单击“搜索”链接,你可以输入以下内容:

Links like this may not have a name, id, or data attribute that you can use to identify them. But you can use the next best thing—the text of the link itself. To click on the Search link, you could write the following:

driver.findElement(By.linkText("搜索")).click();
driver.findElement(By.linkText("Search")).click();

您还可以搜索链接文本以查找部分匹配项。要单击“我的预订”链接,请执行以下操作:

You can also search the link texts for a partial match. To click on the My Bookings link, the following call would work:

驱动程序.findElement(By.partialLinkText("预订"))。点击();
driver.findElement(By.partialLinkText("Bookings")).click();

通过文本内容识别链接很简单、直观,而且相对可靠,但如果显示的文本是修改的。

Identifying links by their text content is simple, intuitive, and relatively robust, though the test will obviously break if the displayed text is modified.

使用 CSS 识别元素

Identifying elements using CSS

一个识别元素的更灵活的方式是使用CSS 选择器,这种模式旨在识别网页的不同部分,以进行格式化和样式设置,但它们也是识别页面元素的一种很好的通用方式。

A more flexible way of identifying elements is to use CSS selectors, patterns designed to identify different parts of a web page for formatting and styling, but they’re also a great general-purpose way to identify elements on the page.

例如,在图 10.7 中,欢迎消息文本仅通过其 CSS 类进行标识。您可以使用以下By.cssSelector()方法使用 CSS 选择器查找 Web 元素在 CSS 中,可以使用以下符号来标识具有给定类的元素".",如下所示:

For example, in figure 10.7, the welcome message text is only identified by its CSS class. You can find web elements with CSS selectors by using the By.cssSelector() method. In CSS, you identify an element with a given class by using the "." notation, like this:

驱动程序.findElement(By.cssSelector(“.welcome-message”));
driver.findElement(By.cssSelector(".welcome-message"));

我们也可以使用以下By.className()方法来实现。但是,当你需要查找没有干净的 Web 元素时,CSS 选择器会变得更有价值idname属性,或者元素嵌套在其他元素中。例如,如果页面上有几条欢迎消息,并且我们想在 welcome-container 组件内识别欢迎消息(见图 10.7),我们可以编写如下代码:

We could also do this using the By.className() method. But CSS selectors become more valuable when you need to find web elements without clean id or name attributes, or where elements are nested inside other elements. For example, if there were several welcome messages on the page and we wanted to identify the welcome message inside the welcome-container component (see figure 10.7), we could write something like this:

驱动程序.findElement(By.cssSelector(“.welcome-container .welcome-message”));
driver.findElement(By.cssSelector(".welcome-container .welcome-message"));

表 10.2 列出了一些比较有用的 CSS 选择器。

Some of the more useful CSS selectors are listed in table 10.2.

表 10.2 有用的 CSS 选择器

Table 10.2 Useful CSS selectors

选择器

Selector

例子

Example

笔记

Notes

.class

.class

.navbar

.navbar

匹配所有具有该类的元素navbar

Matches all elements with the class navbar

#id

#id

#welcome-message

#welcome-message

id匹配带有of 的元素welcome-message

Matches the element with an id of welcome-message

tag

tag

img

img

匹配所有<img>元素

Matches all the <img> elements

element element

element element

.navbar a

.navbar a

<a>匹配元素内具有该类的所有元素navbar

Matches all the <a> elements inside an element with the class navbar

element > element

element > element

.navbar-header > a

.navbar-header > a

匹配<a>元素下具有以下类的元素navbar-header

Matches <a> elements directly under an element with the class navbar-header

[attribute=value]

[attribute=value]

a[href="#/book"]

a[href="#/book"]

匹配<a>元素href值为#/book

Matches <a> elements with an href value of #/book

[attribute^=value]

[attribute^=value]

a[href^="#"]

a[href^="#"]

匹配以下列值开头的<a>元素href#

Matches <a> elements with an href value that starts with #

[attribute$=value]

[attribute$=value]

a[href$="book"]

a[href$="book"]

匹配以下列值结尾的<a>元素hrefbook

Matches <a> elements with an href value that ends in book

[attribute*=value]

[attribute*=value]

a[href*="book"]

a[href*="book"]

匹配包含以下内容的值的<a>元素hrefbook

Matches <a> elements with an href value that contains book

:nth-child(n)

:nth-child(n)

.navbar li:nth-child(3)

.navbar li:nth-child(3)

<li>匹配类元素中的第三个navbar

Matches the third <li> inside an element of class navbar

让我们看一个更实际的例子。您与营销人员定义的要求之一如下:

Let’s look at a more practical example. One of the requirements you’ve defined with the marketing folk goes along the following lines:

场景:显示特色目的地
鉴于 Jane 已登录
当 Jane 查看主页时
那么她应该会看到 3 个特色目的地
特色目的地应该包括新加坡
Scenario: Displaying featured destinations
Given Jane has logged on
When Jane views the home page
Then she should see 3 featured destinations
And the featured destinations should include Singapore

在常旅客主页上,每个特色目的地都显示在<div>带有 featured 类的元素内。目的地标题嵌套在<span>带有 featured-destination 类的元素内。呈现的 HTML 代码如下所示:

On the Frequent Flyer home page, each featured destination appears inside a <div> element with the featured class. The destination title is nested inside a <span> element with the featured-destination class. The rendered HTML code looks something like this:

<div id="featured">                                           
    <div class="featured-destination"...>                     
        <img src="img/singapore.png"></img>                   
        <span class="destination-title">新加坡</span>      
        <span class="destination-price">900 美元</span>
    </div>
    <div class="featured-destination">...</div>               
    <div class="featured-destination">...</div>               
</div>
<div id="featured">                                          
    <div class="featured-destination"...>                    
        <img src="img/singapore.png"></img>                  
        <span class="destination-title">Singapore</span>     
        <span class="destination-price">$900</span>
    </div>
    <div class="featured-destination">...</div>              
    <div class="featured-destination">...</div>              
</div>

所有特色目的地都出现在此<div>内。

All the featured destinations appear within this <div>.

每个特色目的地都有自己的<div>。

Each featured destination has its own <div>.

设置特色目的地的图片

Sets the featured destination’s image

设置特色目的地的标题

Sets the featured destination’s title

其他类似特色目的地如下。

Other featured destinations like this follow.

在 CSS 中,您可以使用句点 (.) 前缀匹配具有给定类的元素。使用 CSS 选择器,您可以找到<div>代表特色目的地的所有元素,如下所示:

In CSS, you can match elements with a given class by using the period (.) prefix. Using a CSS selector, you could find all the <div> elements that represent the featured destinations like this:

List<WebElement> 目的地                                          
    = driver.findElements(By.cssSelector(“.featured-destination”));     
断言(目的地)hasSize(3);                                    
List<WebElement> destinations                                          
    = driver.findElements(By.cssSelector(".featured-destination"));    
assertThat(destinations).hasSize(3);                                   

查找所有特色目的地元素

Finds all the featured destination elements

检查匹配的特色目的地元素的数量

Checks the number of matching featured destination elements

请注意,您正在使用该findElements()方法而不是findElement()方法你之前看到过。顾名思义,该findElements()方法返回匹配的 Web 元素列表,而不仅仅是单个元素。然后,您可以使用AssertJ库检查返回列表的大小https://assertj.github.io/doc/)以使测试更具可读性。

Note that you’re using the findElements() method rather than the findElement() method you saw previously. As the name suggests, the findElements() method returns a list of matching web elements, rather than just a single one. You then check the size of the returned list, using the AssertJ library (https://assertj.github.io/doc/) to make the test more readable.

如果您只是想计算特色目的地的数量,这就足够了,但如果您需要检查目的地标题,则需要进一步深入研究。幸运的是,CSS 选择器非常灵活。您可以通过查找具有 destination-title 类的所有 Web 元素直接检索标题:

This would be enough if you just wanted to count the number of featured destinations, but if you need to check the destination titles, you’ll need to drill further. Fortunately, CSS selectors are flexible. You could retrieve the titles directly by finding all the web elements with the destination-title class:

驱动程序.findElements(By.cssSelector(“.destination-title”));
driver.findElements(By.cssSelector(".destination-title"));

这种方法虽然有效,但可能不够可靠。如果目的地标题在页面的其他地方使用,您将检索到太多标题。更安全的方法是将搜索限制在<div>包含所有特色目的地的嵌套元素中:

This would work, but it may not be robust. If destination titles were used elsewhere on the page, you’d retrieve too many titles. A safer approach would be to limit your search to the elements nested within the <div> that contains all the featured destinations:

驱动程序.findElements(By.cssSelector(“#featured .destination-title”));
driver.findElements(By.cssSelector("#featured .destination-title"));

获得匹配的 Web 元素列表后,您需要将其转换为可以验证的字符串列表。您可以编写如下代码:

Once you have a list of matching web elements, you need to convert it to a list of strings that you can verify. You could write something like this:

List<WebElement> 目的地                                               
  = driver.findElements(By.cssSelector(“#featured .destination-title”));     
List<String> destinationTitles = new ArrayList<String>();                    
for(WebElement 目的地元素:目的地){                          
    目的地标题.添加(目的地元素.getText());                     
}                                                                            
assertThat(destinationTitles).contains("新加坡");                         
List<WebElement> destinations                                               
  = driver.findElements(By.cssSelector("#featured .destination-title"));    
List<String> destinationTitles = new ArrayList<String>();                   
for(WebElement destinationElement : destinations) {                         
    destinationTitles.add(destinationElement.getText());                    
}                                                                           
assertThat(destinationTitles).contains("Singapore");                        

查找所有特色目的地标题元素

Finds all featured destination title elements

将这些 Web 元素转换为字符串列表

Converts these web elements into a list of strings

使用 getText() 提取每个元素的字符串内容

Uses getText() to extract the string contents of each element

检查列表内容

Checks the contents of the list

首先,检索匹配的 Web 元素列表。要获取 的文本内容WebElement,请使用getText()方法,因此循环遍历 Web 元素并提取每个元素的文本内容。最后,检查目的地标题是否确实包含“新加坡”。

First, retrieve the list of matching web elements. To get the text content of a WebElement, you use the getText() method, so loop through the web elements and extract the text contents of each one. Finally, check that the destination titles do indeed contain “Singapore.”

我们在这里只是对 CSS 选择器进行了粗略的介绍,但它们对于使用现代基于 jQuery 的 UI 框架非常有用。您可以在 W3 网站 ( http://www.w3.org/TR/CSS21/selector.xhtml ) 上找到更多详细信息。大多数现代浏览器都对 CSS 提供了出色的原生支持选择器,这意味着使用 CSS 选择器的测试通常非常快速地。

We’ve just scratched the surface of CSS selectors here, but they’re very useful for working with modern jQuery-based UI frameworks. You can find more details on the W3 website (http://www.w3.org/TR/CSS21/selector.xhtml). Most modern browsers have excellent native support for CSS selectors, which means that tests using CSS selectors will generally be very fast.

使用 XPath 识别元素

Identifying elements using XPath

CSS 选择器灵活而优雅,但它们有时会遇到限制。更强大的替代方法是使用 XPath,这是一种用于选择 XML 文档中元素的查询语言。XPath表达式是路径类结构,它根据元素的相对位置、属性值和内容描述页面中的元素。在 WebDriver 上下文中,您可以使用 XPath 表达式选择 HTML 页面结构中的任意元素。表 8.3 中列出了有用的 XPath 表达式。

CSS selectors are flexible and elegant, but they do run into limits from time to time. A more powerful alternative is to use XPath, a query language designed to select elements in an XML document. XPath expressions are path-like structures that describe elements within a page based on their relative position, attribute values, and content. In the context of WebDriver, you can use XPath expressions to select arbitrary elements within the HTML page structure. A list of useful XPath expressions can be found in table 8.3.

表 10.3 有用的 XPath 表达式

Table 10.3 Useful XPath expressions

XPath 表达式

XPath expression

例子

Example

笔记

Notes

node

node

a

a

匹配所有<a>元素

Matches all of the <a> elements

//node

//node

//button

//button

<button>匹配文档根目录下的所有元素

Matches all of the <button> elements somewhere under the document root

//node/node

//node/node

//button/span

//button/span

匹配位于元素<span>正下方的元素<button>

Matches <span> elements that are situated directly under a <button> element

[@attribute=value]

[@attribute=value]

//a[@class='navbar-brand']

//a[@class='navbar-brand']

匹配属性完全等于<a>的元素classnavbar-brand

Matches <a> elements whose class attribute is exactly equal to navbar-brand

[contains(@attribute,value)]

[contains(@attribute,value)]

//div[contains(@class,'navbar-header')]

//div[contains(@class,'navbar-header')]

匹配属性包含表达式<div>的元素classnavbar-header

Matches <div> elements whose class attribute contains the expression navbar-header

node[n]

node[n]

//div[@id='main-navbar']//li[3]

//div[@id='main-navbar']//li[3]

用of匹配<li>里面的第三个<div>idmain-navbar

Matches the third <li> inside the <div> with an id of main-navbar

[.=value]

[.=value]

//h2[.='Flying High Frequent Flyers']

//h2[.='Flying High Frequent Flyers']

匹配<h2>元素的文本内容等于Flying High Frequent Flyers

Matches the <h2> element with text contents equal to Flying High Frequent Flyers

By.xpath()您可以使用方法通过 XPath 表达式查找元素。您可以使用以下表达式找到欢迎消息标题:

You can find an element via an XPath expression by using the By.xpath() method. You could find the welcome message heading by using the following expression:

驱动程序.findElement(By.xpath(“//h3[@id='welcome-message']”));
driver.findElement(By.xpath("//h3[@id='welcome-message']"));

XPath 需要比 CSS 更多的文档结构知识,并且它无法从 CSS 选择器中内置的对 HTML 的深入了解中获益。这使得简单选择器比 CSS 中的对应选择器更冗长。例如,在上一节中,您了解了如何使用以下 CSS 选择器查找特色目标标题列表:

XPath requires more knowledge of the document structure than CSS, and it doesn’t benefit from the intimate understanding of HTML that’s built into CSS selectors. This makes simple selectors more verbose than their equivalents in CSS. For example, in the previous section you saw how you could find the list of featured destination titles using the following CSS selector:

驱动程序.findElements(By.cssSelector(“.destination-title”));
driver.findElements(By.cssSelector(".destination-title"));

XPath 中的等效项可能是这样的:

The equivalent in XPath might be something like this:

驱动程序.findElements(By.xpath(“//span[@class='destination-title']”));
driver.findElements(By.xpath("//span[@class='destination-title']"));

这将查找<span>网页上任何位置具有名为class它等于目的地标题。

This will find all the <span> elements anywhere on the web page that have an attribute named class that’s equal to destination-title.

您可以使用通配符 ( *) 代替以下内容,使其更加通用span

You could make this more generic by using a wildcard (*) instead of span:

驱动程序.findElements(By.xpath(“//*[@class='destination-title']”));
driver.findElements(By.xpath("//*[@class='destination-title']"));

这将找到类完全等于的元素destination-title

This would find elements whose class was exactly equal to destination-title.

不幸的是,现代 Web 应用程序有时会向class属性添加额外的类,因此您不能依赖精确匹配。更可靠的解决方案是使用 XPathcontains()函数,匹配具有class属性的元素其值包含destination-title

Unfortunately, modern web applications will sometimes add extra classes to the class attribute, so you can’t rely on an exact match. A more reliable solution is to use the XPath contains() function, matching elements that have a class attribute with a value that contains destination-title:

驱动程序.findElements(By.xpath(“//*[contains(@class,'destination-title')]”));
driver.findElements(By.xpath("//*[contains(@class,'destination-title')]"));

当您需要根据内容查找元素时,XPath 的全部功能就会变得更加明显(CSS 目前不支持此功能)。例如,您之前看到的精选目的地在 HTML 中的呈现方式如下:

The full power of XPath becomes more apparent when you need to find elements based on their content—something that’s not currently supported in CSS. For example, the featured destinations you saw earlier are rendered in HTML like this:

<div class="特色目的地"...>
    <img src="img/新加坡.png"></img>
    <span class="destination-title">新加坡</span>
    <span class="destination-price">900 美元</span>
</div>
<div class="featured-destination">...</div>
<div class="featured-destination"...>
    <img src="img/singapore.png"></img>
    <span class="destination-title">Singapore</span>
    <span class="destination-price">$900</span>
</div>
<div class="featured-destination">...</div>

您可以使用以下 XPath 表达式找到<span>包含文本的元素:Singapore

You could find the <span> element containing the text Singapore using the following XPath expression:

//span[.='新加坡']
//span[.='Singapore']

您可以进一步研究。假设您需要查找特色目的地所显示的价格Singapore。XPath 支持使用符号的相对路径,因此您可以像这样使用 destination-price 类来".."查找相邻的符号:<span>

You could take this further. Suppose you need to find the price displayed for the Singapore featured destination. XPath supports relative paths using the ".." notation, so you could find the neighboring <span> notation with a class of destination-price like this:

//span[.='新加坡']/../span[contains(@class,'destination-price')]
//span[.='Singapore']/../span[contains(@class,'destination-price')]

XPath 并非没有缺点。XPath 表达式通常比 CSS 选择器更冗长,可读性更差。如果编写不当,XPath 表达式也可能很脆弱。Internet Explorer 本身不支持 XPath,因此在 Internet Explorer 上使用 XPath 的测试可能会运行得非常慢。但 XPath 比 CSS 更强大,在某些情况下,XPath 是可靠识别您正在寻找的元素的唯一方法为了。

XPath isn’t without its disadvantages. XPath expressions are generally more verbose and less readable than CSS selectors. XPath expressions can also be fragile if they aren’t well crafted. XPath has no native support in Internet Explorer, so tests that use XPath on Internet Explorer may run very slowly. But XPath is more powerful than CSS, and there are cases where XPath will be the only way to reliably identify the elements you’re looking for.

使用嵌套查找

Using nested lookups

什么时候当您使用 WebDriver 编写自动化测试时,重要的是保持表达式尽可能简单易读。更简单的表达式往往更容易理解和维护,而且在许多情况下它们更可靠。编写更简单的 WebDriver 代码时,一个有用的策略是使用嵌套查找。

When you write automated tests with WebDriver, it’s important to keep expressions as simple and readable as possible. Simpler expressions tend to be easier to understand and to maintain, and in many cases they’re more reliable. One useful strategy when it comes to writing simpler WebDriver code is to use nested lookups.

到目前为止,您已使用WebDriver实例找到了 Web 元素。但您也可以调用findElement()findElements()方法直接作用于WebElement实例。例如,假设有多个Book链接在页面上的不同位置,你需要点击Book链接在主菜单中。您可以先使用其 ID 查找主菜单,然后使用linkText选择器在主菜单的 Web 元素中查找菜单条目

So far, you’ve found web elements using the WebDriver instance. But you can also call the findElement() and findElements() methods directly on WebElement instances. For example, suppose there are several Book links at different places on the page, and you need to click on the Book link in the main menu. You could do this by first finding the main menu using its ID and then finding the menu entry within the main menu’s web element using the linkText selector:

driver.findElement(By.id("main-navbar"))      
      .findElement(By.linkText("书"))       
      .点击();                               
driver.findElement(By.id("main-navbar"))     
      .findElement(By.linkText("Book"))      
      .click();                              

将元素搜索范围缩小到主导航栏

Narrows down the search for elements to the main navbar

在导航栏中查找链接

Looks for a link within the navbar

点击链接

Clicks on the link

这种方法清晰直观,比使用复杂的 XPath 表达式或CSS选择器。

This approach is clear and intuitive and tends to be less error-prone than using complex XPath expressions or CSS selectors.

10.2.13 交互使用网页元素

10.2.13 Interacting with web elements

在 WebDriver 中与 Web 元素交互通常相当直观,并且涉及的方法相对较少。您可以使用click()方法在任何 Web 元素(不仅仅是按钮和链接)上模拟鼠标点击。该sendKeys()方法可用于模拟用户输入。并且getAttributeValue()getText()方法让您检索 Web 元素的属性值和文本内容。

Interacting with web elements in WebDriver is usually fairly intuitive, and it involves a relatively small number of methods. You can use the click() method on any web element (not just buttons and links) to simulate a mouse click. The sendKeys() method can be used to simulate user input. And the getAttributeValue() and getText() methods let you retrieve attribute values and the text contents of a web element.

图 10.8 所示的常旅客注册页面有许多字段可以说明这些想法。让我们来看看上一章中看到的一个场景,看看它是如何工作的。我们将自动化的场景如下:

The Frequent Flyer registration page illustrated in figure 10.8 has many fields that can illustrate these ideas. Let’s walk through one of the scenarios we saw in the previous chapter to see how this works. The scenario we will automate is the following:

规则:客户必须注册才能使用飞行常客计划会员 
区域
示例:Trevor 注册成为常旅客会员
  鉴于Trevor 没有飞行常客账户
  他注册成为飞行常客会员
时  然后Trevor 应该能够登录常旅客应用程序
  应该有一个飞行常客帐户:
    | 状态级别 | 标准 |
    | 积分 | 0 |
Rule: Customers must register to be able to use the Frequent Flyer members 
 area
Example: Trevor registers as a Frequent Flyer member
  Given Trevor does not have a Frequent Flyer account
  When he registers as a Frequent Flyer member
  Then Trevor should be able to log on to the Frequent Flyer application
  And he should have a Frequent Flyer account with:
    | Status Level | STANDARD |
    | Points       | 0        |

图 10.8 常旅客注册页面

Figure 10.8 The Frequent Flyer registration page

让我们看看如何实现每个步骤的自动化。

Let’s see how we automated each of these steps.

准备测试数据

Preparing the test data

第一步只是定义我们将在此场景中使用的角色。我们在第 9 章中看到了这种方法。

The first step simply defines the persona we will be using for this scenario. We saw this approach in chapter 9.

鉴于 Trevor 没有飞行常客账户
Given Trevor does not have a Frequent Flyer account

步骤定义方法使用名为Traveler,它定义了我们在注册过程中需要使用的所有字段:

The step definition method uses a persona class called Traveler, which defines all the fields we need to use in the registration process:

旅行者新会员;
 
@Given("{} 没有飞行常客帐户")
公共无效notAFrequentFlyerMember(字符串名称){
    新会员 = TravelerPersonas.findByName(姓名);
}
Traveler newMember;
 
@Given("{} does not have a Frequent Flyer account")
public void notAFrequentFlyerMember(String name) {
    newMember = TravelerPersonas.findByName(name);
}

实际值在 HOCON 配置文件中定义,如下所示:

The actual values are defined in a HOCON configuration file, and look like this:

特雷弗:{
  名字:“Trevor”
  姓氏:“旅行者”
  电子邮件:“trevor@traveler.com”
  密码:“tr3vor”
  头衔:“先生”
  地址:“10 Partridge Street, Dandenong”
  国家:“澳大利亚”
  座位偏好:“过道”
}
Trevor: {
  firstName: "Trevor"
  lastName: "Traveler"
  email: "trevor@traveler.com"
  password: "tr3vor"
  title:"Mr"
  address: "10 Partridge Street, Dandenong"
  country: "Australia"
  seatPreference: "Aisle"
}

现在让我们看看如何使用这个角色对象与注册页面进行交互:

Now let’s see how we use this persona object to interact with the registration page:

当他注册成为飞行常客会员时
When he registers as a Frequent Flyer member

在此步骤中,我们将填写并提交申请表,并向 Trevor 提交细节。

Inside this step, we will complete and submit the application form with Trevor’s details.

使用文本字段

Working with text fields

第一个任务是完成电子邮件、名字和姓氏等输入字段。要在文本字段中输入值,可以使用sendKeys()方法,如下所示:

The first task is to complete the input fields such as email, first name, and last name. To enter a value into a text field, you can use the sendKeys() method, as shown here:

驱动程序.findElement(By.id("email"))。sendKeys(newMember.getEmail());
driver.findElement(By.id("email")).sendKeys(newMember.getEmail());

方法sendKeys()不设置字段的值;而是模拟用户在字段中输入文本。如果字段包含现有值,则需要使用clear()方法在输入新值之前。

The sendKeys() method doesn’t set the value of the field; rather, it simulates the user typing the text into the field. If the field contains an existing value, you’ll need to use the clear() method before entering the new value.

有时,我们需要模拟特定的键盘操作,例如按 Enter 或 Tab 键。我们可以简单地使用 Selenium Keys 类来实现这一点,该类为这些特殊键提供了一组值。例如,我们可以在建议的国家/地区列表中选择国家/地区值的一种方法是键入国家/地区的名称,然后按 Tab 键。以下是我们可以执行的操作这:

Occasionally, we need to simulate specific keyboard actions, such as hitting the Enter or Tab key. We can do that by simply using the Selenium Keys class, which gives us a set of values for these special keys. For example, one way we can select the country value in the list of proposed countries is to type the name of the country and then press Tab. Here’s how we could do this:

    驱动程序.findElement(By.id("country"))。sendKeys(newMember.getCountry(), 
                                              键.TAB);
    driver.findElement(By.id("country")).sendKeys(newMember.getCountry(), 
                                              Keys.TAB);

从文本字段读取值

Reading values from text fields

有时我们可能需要检查文本字段中包含的当前值。这存储在相应输入元素的 value 属性中。要检索属性值,可以使用getAttribute()方法,如下所示:

Sometimes we might need to check the current value contained in a text field. This is stored in the value attribute of the corresponding input element. To retrieve an attribute value, you can use the getAttribute() method, as shown here:

字符串电子邮件 = 驱动程序.findElement(By.id(“电子邮件”))。getAttribute(“值”);
String email = driver.findElement(By.id("email")).getAttribute("value");

此方法也适用于使用 value 属性的任何其他表单字段,例如复选框或较新的 HTML5 输入字段类型(如电子邮件和日期)。例外情况是<textarea>没有 value 属性的字段。您可以<textarea>使用getText()方法

This approach will also work for any other form field that uses the value attribute, such as check boxes or the newer HTML5 input field types like email and date. The exception is <textarea> fields, which don’t have a value attribute. You can retrieve the contents of a <textarea> field by using the getText() method.

单选按钮和复选框

Radio buttons and check boxes

选择单选按钮值的最简单方法是找到所需的单选按钮并单击它。这可能有点棘手,因为name属性不是唯一的,并且id属性并不总是定义或与值直接相关。例如,我们的注册表单中单选按钮的 HTML 代码如下所示:

The simplest way to select a radio button value is to find the radio button you want and to click on it. This can be a little tricky because the name attribute isn’t unique, and the id attribute isn’t always defined or directly related to the value. For example, the HTML code for the radio buttons in our registration form looks like this:

<input type="radio" id="aisle" formcontrolname="seatPreference"
       name="seatPreference" value="aisle" data-testid="seat-aisle">
<label for="aisle">过道</label>
<input type="radio" id="window" formcontrolname="seatPreference"
       名称=“seatPreference”值=“window”数据测试ID=“seat-window”>
<label for="window">窗口</label>
<input type="radio" id="aisle" formcontrolname="seatPreference"
       name="seatPreference" value="aisle" data-testid="seat-aisle">
<label for="aisle">Aisle</label>
<input type="radio" id=" window" formcontrolname="seatPreference"
       name="seatPreference" value="window" data-testid="seat-window">
<label for="window">Window</label>

ame在这种情况下,一种方法可能是将 value 属性与 n属性结合使用(标识单选按钮组):

One approach in this case might be to use the value attribute in conjunction with the name attribute (which identifies the group of radio buttons):

驱动程序.findElement(
    By.cssSelector("input[name='seatPreference'][value='aisle']")
)。点击()
driver.findElement(
    By.cssSelector("input[name='seatPreference'][value='aisle']")
).click()

我们也可以选择单击相应的标签元素,如此例所示,我们使用 XPath 表达式来识别 Window 标签元素:

We might also opt to click on the corresponding label element, as shown in this example where we use an XPath expression to identify the Window label element:

驱动程序.findElement(By.xpath("//label[.='Window']"))。click()
driver.findElement(By.xpath("//label[.='Window']")).click()

在我们的应用程序中,我们可以从传递给步骤定义方法的角色属性中动态创建一个 XPath 选择器:

And in the case of our application, we could create an XPath selector dynamically from the persona attributes we pass to the step definition method:

String seatPreference = String.format("//标签[.='%s']",
                                      新会员.获取座位偏好());
驱动程序.findElement(By.xpath(seatPreference))。点击();
String seatPreference = String.format("//label[.='%s']",
                                      newMember.getSeatPreference());
driver.findElement(By.xpath(seatPreference)).click();

这种方法也适用于复选框,其行为完全相同方式。

This approach will also work for check boxes, which behave in exactly the same way.

下拉列表列表

Drop-down lists

下拉列表是 Web 界面的常见元素。它们可以采用传统 HTML SELECT 元素的形式,也可以采用特定于框架的 JavaScript 组件的形式。

Drop-down lists are a common element of web interfaces. These can come in the form of traditional HTML SELECT elements or as framework-specific JavaScript components.

对于 SELECT 元素,WebDriver 提供了一个方便的辅助类来处理下拉列表。Select该类用于包装表示下拉列表的 Web 元素,以添加特定于下拉列表的方法,例如selectByVisibleText()selectByValue(), 和selectByIndex()在预订页面,您可以"Business"使用以下代码将旅行舱位设置为:

For SELECT elements, WebDriver provides a convenient helper class for dealing with drop-down lists. The Select class is used to wrap a web element representing a drop-down list to add drop-down-specific methods, such as selectByVisibleText(), selectByValue(), and selectByIndex(). On the booking page, you could set the travel class to "Business" using the following code:

WebElement travelClassElt = getDriver().findElement(By.id("travel-class"));
新的选择(travelClassElt).selectByVisibleText(“商务”);
WebElement travelClassElt = getDriver().findElement(By.id("travel-class"));
new Select(travelClassElt).selectByVisibleText("Business");

班级Select还提供了许多可用于了解下拉列表当前状态的方法,包括getFirstSelectedOption() getAllSelectedOptions()

The Select class also provides a number of methods that you can use to learn about the current state of the drop-down list, including getFirstSelectedOption() and getAllSelectedOptions().

10.2.14 使用现代 UI 库组件

10.2.14 Working with modern UI library components

更多的现代应用程序经常使用非标准 HTML 标签来实现 UI 组件。例如,我们的 Frequent Flyer 应用程序使用Angular Materialhttps://material.angular.io/),一个用于 Angular JS 应用程序的 UI 组件库。您可以在图 10.9 中看到其中一个组件的示例,该组件显示了注册页面上的标题下拉列表。让我们看看如何使用 Selenium WebDriver 自动化此类组件。

More modern applications often use nonstandard HTML tags to implement UI components. For example, our Frequent Flyer application uses the Angular Material library (https://material.angular.io/), a UI component library for Angular JS applications. You can see an example of one of these components in figure 10.9, which shows the Title dropdown list on the registration page. Let’s see how we would automate this sort of component using Selenium WebDriver.

虽然该Title字段看起来像 HTML SELECT 字段,但相应的 HTML 代码却大不相同。下拉字段使用mat-select标签,看起来像这样:

Although the Title field looks like an HTML SELECT field, the corresponding HTML code is quite different. The dropdown field uses the mat-select tag, and looks something like this:

<mat-select role="listbox" id="title" data-testid="title">
    ...
</mat-select>
<mat-select role="listbox" id="title" data-testid="title">
    ...
</mat-select>

当你点击下拉字段时,类似于以下的 HTML 元素块会动态添加到 DOM 中,显示你在图 10.9 中看到的下拉值:

When you click on the dropdown field, a block of HTML elements similar to the following are dynamically added to the DOM, showing the dropdown values you see in figure 10.9:

<div class="mat-select-panel-wrap">
    <div class="mat-select-panel mat-primary" id="title-panel">
        <mat-option 角色="option" 值="先生" 类="mat-option">
            <span class="mat-option-text">先生</span>
        </mat-选项>
        <mat-option 角色="option" 值="Ms" 类="mat-option">
            <span class="mat-option-text">女士</span>
        </mat-选项>
        <mat-option 角色="option" 值="夫人" 类="mat-option">
            <span class="mat-option-text">夫人</span>
        </mat-选项>
    </div>
</div>
<div class="mat-select-panel-wrap">
    <div class="mat-select-panel mat-primary" id="title-panel">
        <mat-option role="option" value="Mr" class="mat-option">
            <span class="mat-option-text">Mr</span>
        </mat-option>
        <mat-option role="option" value="Ms" class="mat-option">
            <span class="mat-option-text">Ms</span>
        </mat-option>
        <mat-option role="option" value="Mrs" class="mat-option">
            <span class="mat-option-text">Mrs</span>
        </mat-option>
    </div>
</div>

图 10.9 许多现代 Web 应用程序中发现的非标准 HTML UI 组件的示例

Figure 10.9 An example of a nonstandard HTML UI component found in many modern web applications

要在下拉列表中选择一个值,我们首先需要单击mat-select元素,然后点击相应的mat-option条目

To select a value in the dropdown, we first need to click on the mat-select element, and only then click on the corresponding mat-option entry.

第一步很简单。我们只需点击mat-select元素,我们可以这样做:

The first step is easy. We simply click on the mat-select element, which we could do like this:

    驱动程序.findElement(By.id("title"))。click();
    driver.findElement(By.id("title")).click();

对于第二步,我们有几个选项。一个可能是单击mat-option元素,它有一个方便的值字段。例如,我们可以使用以下代码来识别'Mr'条目:

For the second step, we have a couple of options. One might be to click on the mat-option element, which has a convenient value field. For example, we could use the following code to identify the 'Mr' entry:

    驱动程序.findElement(By.cssSelector("mat-option[value='Mr']").click();
    driver.findElement(By.cssSelector("mat-option[value='Mr']")).click();

或者,我们可能希望避免使用值标签(在所有情况下,它可能不是人类可读的值),而是使用显示的文本值。我们可以使用 XPath 表达式而不是我们显示的 CSS 表达式来做到这一点:

Alternatively, we might want to avoid using the value label (which might not be a human-readable value in all cases) and use the displayed text value instead. We could do this using an XPath expression instead of the CSS one we showed:

driver.findElement(By.xpath("//*[@class='mat-option-
文本'][.='先生']")).click();
driver.findElement(By.xpath("//*[@class='mat-option-
 text'][.='Mr']")).click();

最后一步是让这个表达式依赖于我们的测试数据,我们可以使用与座位偏好类似的方法来实现。

The last step would be to make this expression depend on our test data, which we could do using an approach similar to the one we used for seat preference.

    String titleOption = String.format("mat-option[value='%s']",
                                       新会员.获取标题());
    驱动程序.findElement(By.cssSelector(titleOption))。click();
    String titleOption = String.format("mat-option[value='%s']",
                                       newMember.getTitle());
    driver.findElement(By.cssSelector(titleOption)).click();

这个过程在使用这种非标准 UI 框架时相当典型。但是,有一个方面我们没有考虑到:下拉列表可能不会立即出现。它可能从后端服务检索列表,并在列表出现之前占用一个短暂的实例。在这种情况下,我们刚刚编写的代码可能会失败,因为它要查找的下拉列表条目尚未填充。在下一节中,我们将学习如何处理这种情况以及其他需要等待事情完成的情况发生。

This process is fairly typical of working with nonstandard UI frameworks like this. However, there is one aspect we haven’t considered: the dropdown list may not appear immediately. It may retrieve the list from a backend service and take a short instance before the list appears. In this case, the code we just wrote may fail because the dropdown list entry it is looking for hasn’t been populated yet. In the next section we will learn how to deal with this situation as well as other cases where we need to wait for things to happen.

10.2.15 使用异步页面和测试 AJAX 应用程序

10.2.15 Working with asynchronous pages and testing AJAX applications

最多现代网络应用程序以某种方式使用 AJAX。基于 AJAX 的 JavaScript 库允许开发人员编写可用性和用户体验大大提高的应用程序。但是,AJAX 的异步特性在自动化网络测试方面可能会带来挑战。

Most modern web applications use AJAX in one way or another. AJAX-based JavaScript libraries allow developers to write applications with vastly improved usability and user experience. But the asynchronous nature of AJAX can present challenges when it comes to automated web testing.

在传统的 Web 应用程序中,当您单击链接或提交表单时,会向服务器发送 HTTP 请求并返回新页面。在这些情况下,WebDriver 会自动等待新页面加载后再继续。但是对于 AJAX 应用程序,网页会向服务器发送查询并直接更新页面,而无需重新加载。发生这种情况时,WebDriver 将不知道是否或何时需要等待更新,这可能会导致测试意外失败。

In a conventional web application, when you click on a link or submit a form, an HTTP request is sent to the server and a new page is returned. In these cases, WebDriver will automatically wait for the new page to load before proceeding. But with an AJAX application, the web page will send queries to a server and update the page directly, without reloading. When this happens, WebDriver will not know if or when it needs to wait for updates, which may cause the test to fail unexpectedly.

即使不涉及数据加载,时间问题也会出现。例如,许多 UI 组件库都使用动画;流行的Toastr例如, ( https://codeseven.github.io/toastr/ ) 库使用淡入淡出动画来显示非阻塞通知消息。我们之前看到的注册场景中使用了这种动画:

Timing problems can also come into play even when data loading is not involved. For example, many UI component libraries use animation; the popular Toastr (https://codeseven.github.io/toastr/) library for example uses fading animation for nonblocking notification messages. This kind of animation is used in the registration scenario we saw earlier:

然后 Trevor 应该能够登录常旅客应用程序
Then Trevor should be able to log on to the Frequent Flyer application

当 Trevor 登录到他的新帐户时,会短暂出现一条通知消息(见图 10.10)。该消息逐渐出现,然后在几秒钟后消失。

When Trevor logs on to his new account, a notification message appears briefly (see figure 10.10). The message fades in and then fades away after a few seconds.

图 10.10 用户登录应用程序时短暂出现绿色对话框消息。

Figure 10.10 The green dialog message appears briefly when a user logs on to the application.

上一步检查 Trevor 登录时是否出现此消息并包含正确的电子邮件地址。但如果我们在完成登录过程后立即查找通知消息,则测试将失败;该消息尚未出现。我们需要告诉 Selenium 等待消息出现。WebDriver 为我们提供了一些有关如何执行此操作的选项。

The previous step checks that when Trevor logs on, this message appears and contains the correct email address. But if we look for the notification message as soon as we complete the login process, the test will fail; the message hasn’t yet appeared. We need to tell Selenium to wait for the message to appear. WebDriver gives us a few options as to how to do this.

明确且流畅的等待

Explicit and fluent waits

方法是使用显式等待,这让我们等待特定事件。我们可以使用WebDriverWaitExpectedConditions课程.WebDriverWait班级创建初始框架,等待直到满足某些条件或给定的超时时间到期。

One approach is to use explicit waits, which let us wait for specific events. We can do this using the WebDriverWait and ExpectedConditions classes. The WebDriverWait class creates the initial framework, waiting until some condition is met or a given timeout expires.

    WebDriverWait 等待 = 新的 WebDriverWait(驱动程序,Duration.ofSeconds(5));
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));

不过,这个对象本身不会做任何事情;我们需要告诉它等待某事。这就是ExpectedConditions开始发挥作用。该类提供了大量有用的预定义条件,包括等待元素存在(或不存在)、可见(或不可见)、可点击等等。

This object won’t do anything by itself, though; we need to tell it to wait for something. And that’s where the ExpectedConditions class comes into play. This class provides a large number of useful predefined conditions, including waiting for elements to be present (or not present), visible (or invisible), clickable, and so forth.

如果我们想等待 Toastr 成功消息可见,我们只需使用该visibilityOfElementLocated()方法,如下图所示:

If we want to wait for the Toastr success message to be visible, we would simply use the visibilityOfElementLocated() method, as illustrated here:

    等待.直到(
        预期条件.visibilityOfElementLocated(
            通过.cssSelector(“.toast-success”)
    (英文):
    wait.until(
        ExpectedConditions.visibilityOfElementLocated(
            By.cssSelector(".toast-success")
        )
    );

此代码将等待最多五秒钟或直到出现 Toastr 成功消息(以先到者为准)。

This code will wait up to five seconds or until the Toastr success message appears, whichever comes first.

WebDriverWait方法的巧妙运用是它们会返回您等待的元素,因此一旦找到它,您就可以直接与 Web 元素进行交互。在下面的代码示例中,我们等待 Toastr 消息出现,然后检索消息的文本内容,以便我们可以对消息的值做出断言。请注意我们如何使用来自ExceptedConditions类的静态导入使得代码读起来更加流畅。

A neat trick with the WebDriverWait methods is that they return the element you are waiting for as a result, so you can interact directly with the web element once you find it. In the following code sample, we wait for the Toastr message to appear, and then retrieve the text content of the message, so that we can make an assertion about the value of the message. Note how we are using a static import from the ExceptedConditions class to make the code read more fluently.

    WebDriverWait 等待 = 新的 WebDriverWait(驱动程序,Duration.ofSeconds(5));
    字符串 successMessage = wait.until(visibilityOfElementLocated(
                                        通过.cssSelector(".toast-success")))
                             获取文本
 
    assertThat(successMessage).isEqualTo("已登录为"
                                         + 新会员.获取电子邮件());
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
    String successMessage =  wait.until(visibilityOfElementLocated(
                                        By.cssSelector(".toast-success")))
                             .getText();
 
    assertThat(successMessage).isEqualTo("Logged in as "
                                         + newMember.getEmail());

还有许多其他预先捆绑的Wait条件;表 10.4 列出了一些比较常用的。

There are many, many other pre-bundled Wait conditions; some of the more commonly used ones are outlined in table 10.4.

表 10.4 有用的 WebDriver 等待条件

Table 10.4 Useful WebDriver wait conditions

方法(附示例)

Method (with example)

目的

Purpose

visibilityOfElementLocated(By.id("#elt"))

visibilityOfElementLocated(By.id("#elt"))

等待元素出现在页面上并且可见。

Wait for an element to be both on the page and visible.

visibilityOfNestedElementsLocatedBy(

visibilityOfNestedElementsLocatedBy(

        By.id("#parent"),By.id("#elt")

        By.id("#parent"),By.id("#elt")

)

)

等待子元素出现在页面上并且可见。

Wait for a child element to be both on the page and visible.

textToBePresentInElementLocated(

textToBePresentInElementLocated(

        By.id("elt"),"expected text"

        By.id("elt"),"expected text"

)

)

等待指定文本出现在指定元素中。

Wait for a specified text to be present in a specified element.

invisibilityOfElementLocated(By.id("#elt"))

invisibilityOfElementLocated(By.id("#elt"))

等待元素消失。

Wait for an element to disappear.

invisibilityOfElementWithText(

invisibilityOfElementWithText(

        By.id("#elt"),

        By.id("#elt"),

        "Disappearing text"

        "Disappearing text"

)

)

等待包含某些指定文本的元素消失。

Wait for an element containing some specified text to disappear.

numberOfElementsToBeMoreThan(By.id("#list"), 3)

numberOfElementsToBeMoreThan(By.id("#list"), 3)

等待与指定定位器匹配的元素数量出现。

Wait for the number of elements matching a specified locator to appear.

titleContains("some title")

titleContains("some title")

等到页面标题包含某些指定的值。

Wait until the page title contains some specified value.

urlContains("/search")

urlContains("/search")

等到 URL 包含某个值。

Wait until the URL contains some value.

alertIsPresent()

alertIsPresent()

等待,直到出现 JavaScript 警报对话框。

Wait until a JavaScript Alert dialog appears.

and(

and(

        visibilityOfElementLocated(By.id("#elt-1")),

        visibilityOfElementLocated(By.id("#elt-1")),

        visibilityOfElementLocated(By.id("#elt-2"))

        visibilityOfElementLocated(By.id("#elt-2"))

)

)

等到几个条件都满足。

Wait until several conditions are met.

这些预定义条件涵盖了许多常见情况。但有时您需要做一些更具体的事情。同样,WebDriver 提供了几个选项。该类FluentWait允许您使用可读、流畅的 API 动态创建任意等待参数:

These predefined conditions cover many common situations. But occasionally you’ll need to do something more specific. Again, WebDriver offers several options. The FluentWait class allows you to create arbitrary wait parameters on the fly using a readable, fluent API:

等待 <WebDriver> wait = new FluentWait<>(驱动程序)
    .withTimeout(Duration.ofSeconds(30))             
    .pollingEvery(Duration.ofSeconds(1))             
    .忽略(NoSuchElementException.class);         
Wait<WebDriver> wait = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(30))            
    .pollingEvery(Duration.ofSeconds(1))            
    .ignoring(NoSuchElementException.class);        

等待最多 30 秒。

Wait for up to 30 seconds.

每秒检查一次页面。

Check the page every second.

如果尚未找到元素,请不要失败。

Don’t fail if an element isn’t found yet.

你可以使用这个wait对象ExpectedConditions满足类中的预定义条件之一,或者您可以编写自己的条件。条件采用 Java 8 函数对象的形式,它通常返回WebElement(如果您正在等待 Web 元素可用)或布尔值(如果您正在等待一些更一般的条件)。

You can use this wait object with one of the predefined conditions from the ExpectedConditions class, or you can write your own condition. A condition takes the form of a Java 8 Function object, and it typically returns either a WebElement (if you’re waiting for a web element to become available) or a Boolean (if you’re waiting for some more general condition).

例如,我们可以等到 Toastr 通知消息包含特定格式的电子邮件地址,如下所示:

For example, we could wait until the Toastr notification message contained an email address of a particular format like this:

    等待.直到(司机 
                 -> 驱动程序.findElement(By.cssSelector(“.toast-success”))
                          .获取文本()
                          .包含(“@traveler.com”)
    (英文):
    wait.until(driver 
                 -> driver.findElement(By.cssSelector(".toast-success"))
                          .getText()
                          .contains("@traveler.com")
    );

流畅的等待写起来稍微复杂一些,但在等待方面却提供了无限的可能性为了随意的事件。

Fluent waits are a little more complicated to write, but they open infinite possibilities when it comes to waiting for arbitrary events.

10.3 易于测试的 Web 应用程序

10.3  Test-friendly web applications

Web 应用程序代码的样式和质量对测试的难易程度有很大影响。如果应用程序的 HTML 代码、标识符、名称和 CSS 类对页面上的所有重要元素都清晰,则测试将更加容易和可靠。如果应用程序的 HTML 代码混乱或不一致,元素可能难以识别,从而导致选择器逻辑更加复杂和脆弱。反之亦然:正如我们所见,只需使用测试特定的属性就可以大大提高测试的可靠性。

The style and quality of your web application code has a significant influence on how easy or hard it will be to test. Applications with clean HTML code, identifiers, names, and CSS classes for all the significant elements on a page make testing easier and more reliable. When applications have messy or inconsistent HTML code, the elements can be hard to identify, which results in more complicated and more brittle selector logic. And the opposite is true, too: as we have seen, simply using test-specific attributes can go a long way in making your tests more robust.

您选择的技术堆栈会对可测试性产生重大影响。限制您对呈现的 HTML 的控制的框架是主要的困难来源。

The technology stack you choose can have a major effect on testability. Frameworks that limit the control you have over the rendered HTML are a major source of difficulty.

例如,在 Java Web 应用程序开发中,一些框架会自动生成元素标识符供自己使用,这使得使用最简单、最快的 WebDriver 选择器变得困难。(许多基于 JSF 的框架都属于这一类。)使用 Flash 和 Silverlight 等插件技术的应用程序对 WebDriver 等测试工具来说是不透明的,这也使测试变得非常困难难的。

In Java web application development, for example, some frameworks automatically generate element identifiers for their own use, making it difficult to use the simplest and fastest of the WebDriver selectors. (Many JSF-based frameworks fall into this category.) Applications that use plug-in technologies such as Flash and Silverlight, which are opaque to testing tools like WebDriver, also make testing very difficult.

10.4 下一步

10.4 Next steps

到目前为止,您已经了解了如何使用 Selenium WebDriver 与 Web 应用程序交互以及如何自动化操作用户界面的 Cucumber 验收标准的基础知识。为了简单起见,我们几乎完全专注于 WebDriver API。

So far you have seen the fundamentals of how to interact with a web application using Selenium WebDriver, and how to automate Cucumber acceptance criteria that manipulate the user interface. To keep things simple, we have focused almost entirely on the WebDriver API.

但是,我们使用的编码风格实际上并不适合简单的教程项目。为了理解为什么会出现这种情况,让我们重新回顾一下本章前面实现的步骤定义方法之一:

However, the style of coding we have been using is not really suitable for anything beyond simple tutorial projects. To understand why this is the case, let’s revisit one of the step definition methods we implemented earlier in the chapter:

@Then("{} 应该能够登录到飞行常客应用程序")
公共 void shouldBeAbleToLoginAs(字符串名称){
    WebDriver driver = WebTestSupport.currentDriver();                        
    driver.get(“http://localhost:3000/login”);                                
 
    驱动程序.findElement(By.id("email"))。sendKeys(newMember.getEmail());        
    驱动程序.findElement(By.id("密码")。sendKeys(newMember.getPassword()); 
    driver.findElement(By.id("login-button")).click();                        
 
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));    
    字符串 successMessage                                                     
       = 等待.直到(visibilityOfElementLocated(                               
                        通过.cssSelector(“.toast-success”))                     
                    ).getText();                                              
 
    断言(successMessage)                                                
               .isEqualTo("已登录为 " + newMember.getFirstName());        
}
@Then("{} should be able to log on to the Frequent Flyer application")
public void shouldBeAbleToLoginAs(String name) {
    WebDriver driver = WebTestSupport.currentDriver();                       
    driver.get("http://localhost:3000/login");                               
 
    driver.findElement(By.id("email")).sendKeys(newMember.getEmail());       
    driver.findElement(By.id("password")).sendKeys(newMember.getPassword()); 
    driver.findElement(By.id("login-button")).click();                       
 
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));   
    String successMessage                                                    
       = wait.until(visibilityOfElementLocated(                              
                        By.cssSelector(".toast-success"))                    
                    ).getText();                                             
 
    assertThat(successMessage)                                               
               .isEqualTo("Logged in as " + newMember.getFirstName());       
}

获取 WebDriver 实例

Obtains the WebDriver instance

打开登录页面

Opens the login page

与登录表单交互

Interacts with the login form

等待登录通知消息出现

Waits for the login notification message to appear

检查是否显示正确的消息

Checks that the correct message is displayed

即使对于像这样的简单示例,此处显示的代码也相对较长并且很难维护:

Even for a simple example like this, the code shown here is relatively long and would be quite hard to maintain:

  • 我们在测试逻辑中公开了 WebDriver API,这意味着测试与 WebDriver 实现紧密耦合。如果我们想重构测试(例如,使用 API 调用或其他测试技术),我们需要对代码进行重大更改。

  • We are exposing the WebDriver API in our test logic, which means the test is tightly coupled to the WebDriver implementation. If we wanted to refactor the test (e.g., to use API calls or a different testing technology), we would need major changes to the code.

  • 我们在登录屏幕上导航到的 URL 是硬编码的,这使得在远程服务器上运行测试变得困难。

  • The URL we are navigating to for the login screen is hard-coded, which would make it difficult to run the test on a remote server.

  • 定位器嵌入在我们的测试代码中,这意味着如果这些元素在其他步骤定义方法中使用它,则需要在多个地方维护它们。

  • The locators are embedded in our test code, which means that if these elements are used in other step definition methods, they will need to be maintained in several places.

  • 代码包含实现细节,例如需要等待 Toastr 通知消息出现,这些都不相关。

  • The code contains implementation details, such as the need to wait for the Toastr notification message to appear, which are not relevant.

下面的示例使用了我们将在第 12 章中发现的 Screenplay 模式,可以说,它更好地表达了此步骤的意图,同时将实现细节隐藏在其他类中,以便于查找和维护:

The following sample, which uses the Screenplay Pattern we will discover in chapter 12, arguably does a much better job of expressing the intent of this step while hiding the implementation details in other classes where they will be easier to find and maintain:

@Then("{} 应该能够登录到飞行常客应用程序")
public void shouldBeAbleToLoginAs(Actor oftenFlyer) {
    频繁飞行者.尝试(登录.使用他们的凭据());
    常旅客.应该(
            请参阅(LOGIN_NOTIFICATION_MESSAGE), 
                    containsText("已登录为 " + oftenFlyer.getName()))
    (英文):
}
@Then("{} should be able to log on to the Frequent Flyer application")
public void shouldBeAbleToLoginAs(Actor frequentFlyer) {
    frequentFlyer.attemptsTo(Login.usingTheirCredentials());
    frequentFlyer.should(
            seeThat(the(LOGIN_NOTIFICATION_MESSAGE), 
                    containsText("Logged in as " + frequentFlyer.getName()))
    );
}

在接下来的章节中,我们将学习一些可以帮助你的测试自动化代码更易于维护和更健壮的技术。

In the following chapters, we will learn a number of techniques that can help make your test automation code more maintainable and more robust.

概括

Summary

  • 您可以使用 Selenium WebDriver 自动化 Java 中基于 Web 的应用程序的 UI 验收测试。

  • You can automate UI acceptance tests for web-based applications in Java using Selenium WebDriver.

  • 自动化网络测试是一种强大的测试工具,但应谨慎使用,以免过多地减慢您的测试套件的速度。

  • Automated web tests are a powerful testing tool, but should be used sparingly in order to avoid slowing down your test suite too much.

  • Selenium Webdriver 是最常见和最成熟的开源浏览器自动化库之一。

  • Selenium Webdriver is one of the most common and well-established open source browser automation libraries.

  • 在我们的应用程序中构建可测试性(例如,通过为关键元素包含有意义的idscss类)使得编写可靠、可持续的测试自动化代码变得更加容易。

  • Building testability into our applications (e.g., by including meaningful ids or css classes for key elements) makes it much easier to write reliable, sustainable test automation code.

  • 在测试异步应用程序时,我们可以使用不同类型的等待条件(例如,显式和流畅等待)来等待应用程序处于预期状态后再继续测试。

  • When testing asynchronous applications, we can use different types of wait conditions (e.g., explicit and fluent waits) to wait until the application is in an expected state before proceeding with our test.

在接下来的章节中,我们将介绍一些可用于使 Web 测试自动化代码更健壮、更易于扩展和更易于维持。

In the next chapters we will look at a number of techniques you can use to make your web test automation code more robust, easier to extend, and easier to maintain.


1  在较新版本的 Selenium 中,此功能已集成到 Selenium 库本身中,因此不再需要手动配置驱动程序。

1  In the more recent versions of Selenium, this feature has been integrated into the Selenium library itself, so it is no longer necessary to configure drivers manually.

11 个 UI 层的测试自动化设计模式

11 Test automation design patterns for the UI layer

本章封面

This chapter covers

  • 非结构化测试脚本的局限性
  • The limitations of unstructured test scripts
  • 页面对象和页面组件对象
  • Page Objects and Page Component Objects
  • 建模用户界面和用户行为
  • Modeling user interfaces and user behavior
  • 动作类和 DSL
  • Action classes and DSLs

在上一章中,您学习了如何使用基本的 Selenium WebDriver 代码自动化网页。在本章中,您将学习如何组织和构建 UI 测试代码,使其更易于扩展和维护。

In the previous chapter, you learned how to automate a web page using basic Selenium WebDriver code. In this chapter, you will learn how to organize and structure your UI test code to make it easier to extend and easier to maintain.

11.1 非结构化测试脚本的局限性

11.1 The limitations of unstructured test scripts

因此到目前为止,我们已经使用非常简单的代码示例探索了 WebDriver API。这些示例很好地说明了各种 WebDriver 方法,但您不会想为实际的自动化测试编写这样的代码。例如,要登录 Frequent Flyer 网站,您使用了以下代码:

Thus far we’ve explored the WebDriver API using very simple code samples. These examples work well to illustrate the various WebDriver methods, but you wouldn’t want to write code like this for real-world automated tests. For example, to log on to the Frequent Flyer website, you used the following code:

@When(“他/她使用有效的用户名和密码登录”)
公共 void logsOnWithAValidUsernameAndPassword() {
    WebDriver 驱动程序 = WebTestSupport.currentDriver();
 
    驱动程序.get(“http://localhost:3000”);
    driver.findElement(By.linkText("登录")).click();
    驱动程序.findElement(By.id("email"))。sendKeys(frequentFlyer.email);
    司机.findElement(By.id( “密码”))。sendKeys(frequentFlyer.密码);
    驱动程序.findElement(By.id("登录按钮"))。click();
}
@When("he/she logs on with a valid username and password")
public void logsOnWithAValidUsernameAndPassword() {
    WebDriver driver = WebTestSupport.currentDriver();
 
    driver.get("http://localhost:3000");
    driver.findElement(By.linkText("Login")).click();
    driver.findElement(By.id("email")).sendKeys(frequentFlyer.email);
    driver.findElement(By.id("password")).sendKeys(frequentFlyer.password);
    driver.findElement(By.id("login-button")).click();
}

此代码的扩展性较差。您需要为涉及用户登录的每个场景复制相同或相似的代码行,并且对此逻辑的任何更改都需要在使用它的每个地方进行更新。

This code wouldn’t scale well. You’d need to duplicate the same or similar lines for every scenario involving a user logging on, and any change to this logic would need to be updated at every place that it’s used.

另一个问题是,您将选择器逻辑(如By.id("email"))与我们传递给它的测试数据(frequentFlyer.email)混合在一起。当我们在代码中将两件事放在一起时,很难在不改变另一件事的情况下改变其中一件事。

Another problem is that you’re mixing selector logic (like By.id("email")) with the test data we pass to it (frequentFlyer.email). When we keep two things together in our code, it is hard to change one without risking changing the other.

这也意味着,如果我们想在测试中的其他地方与此字段交互(例如,如果我们想在不同场景中输入不同或无效的电子邮件值),我们需要复制定位器逻辑。而这反过来又意味着更多的维护和更多需要更改的地方,例如,如果电子邮件字段的 ID 需要更改改变。

This also means that if we want to interact with this field somewhere else in our tests (e.g., if we wanted to enter different or invalid email values in a different scenario), we would need to duplicate the locator logic. And that, in turn, means more maintenance and more places that need changing if, for example, the ID of the email field were to change.

11.2 将位置逻辑与测试逻辑分离

11.2 Separating location logic from test logic

一个更好的方法是将选择器逻辑重构到一个地方,以便可以在多个测试中重复使用。例如,您可以通过将选择器逻辑放在一个类中,更清晰地重写我们在上一节中看到的脚本:

A better approach would be to refactor the selector logic into one place so that it can be reused across multiple tests. For example, you could rewrite the script we saw in the previous section more cleanly by placing the selector logic in a single class:

公共类登录页{
    私有静态最终 By EMAIL_FIELD = By.id(“email”);                
    private static final By PASSWORD_FIELD = By.id("密码");          
    私有静态最终 By LOGIN_BUTTON = By.id("login-button");        
 
    私有的最终 WebDriver 驱动程序;                                      
 
    公共登录页(WebDriver 驱动程序){                                 
        这个.司机=司机;
    }
 
    公共无效打开(){                                                 
        驱动程序.get(“http://localhost:3000/login”);
    }
 
    public void signinWithCredentials(String email, String password) {   
        驱动程序.findElement(EMAIL_FIELD)。发送密钥(电子邮件);                 
        driver.findElement(PASSWORD_FIELD).sendKeys(密码);           
        驱动程序.findElement(LOGIN_BUTTON)。click();                        
    }
}
public class LoginPage {
    private static final By EMAIL_FIELD = By.id("email");               
    private static final By PASSWORD_FIELD = By.id("password");         
    private static final By LOGIN_BUTTON = By.id("login-button");       
 
    private final WebDriver driver;                                     
 
    public LoginPage(WebDriver driver) {                                
        this.driver = driver;
    }
 
    public void open() {                                                
        driver.get("http://localhost:3000/login");
    }
 
    public void signinWithCredentials(String email, String password) {  
        driver.findElement(EMAIL_FIELD).sendKeys(email);                
        driver.findElement(PASSWORD_FIELD).sendKeys(password);          
        driver.findElement(LOGIN_BUTTON).click();                       
    }
}

此表单中元素的定位器策略

Locator strategies for the elements in this form

声明一个可用于与浏览器交互的 WebDriver 实例

Declares a WebDriver instance that can be used to interact with the browser

传入 WebDriver 实例

Passes in the WebDriver instance

打开登录页面

Opens the login page

输入用户凭证并登录应用程序

Enters user credentials and signs in to the application

我们不使用硬编码选择器,而是将它们表示为可重用的常量值。

Rather than hard-coding selectors, we represent them as reusable constant values.

此类将您之前看到的代码行包装到一个名为的方法中signin-WithCredentials()。它还提供了一个open()方法才能进入正确的页面。代码需要一个WebDriver实例,它是在构造函数中提供的。

This class wraps the lines of code you saw earlier into a single method called signin-WithCredentials(). It also provides an open() method to get to the right page. The code needs a WebDriver instance, which is provided in the constructor.

注意我们不再对findElement()方法中的定位器进行硬编码。登录过程中使用的元素的定位器策略在类的顶部定义为常量值。这有两个作用。首先,定位器集中在一个地方,如果需要更新,可以更容易地找到它们。其次,通过使用可读的常量名称,当我们阅读方法中的代码时,我们可以专注于代码正在寻找什么元素,而不是如何定位元素。

Notice how we are no longer hard-coding the locators in the findElement() methods. The locator strategies for the elements used in the login process are defined as constant values at the top of the class. This does two things. First, the locators are centralized in one place, which makes them easier to find if they need updating. And second, by using readable constant names, when we read the code in the method we can focus on what element the code is looking for rather than how the element is located.

LoginPage当我们使用类时,此原则也适用,它允许你的测试代码专注于测试数据和操作的意图,而不是如何找到或操纵:

This principle also applies when we use the LoginPage class, which allows your test code to focus on the test data and the intent of the actions rather than on how the individual web elements are found or manipulated:

@When(“他/她使用有效的用户名和密码登录”)
公共 void logsOnWithAValidUsernameAndPassword() {
    登录页 loginPage = 新登录页 (驱动程序);
    登录页面.open();                                                     
    loginPage.signinWithCredentials(“jane.smith@acme.com”,“s3cr3t”;      
}
@When("he/she logs on with a valid username and password")
public void logsOnWithAValidUsernameAndPassword() {
    LoginPage loginPage = new LoginPage(driver);
    loginPage.open();                                                    
    loginPage.signinWithCredentials("jane.smith@acme.com", "s3cr3t";     
}

打开登录页面

Opens the login page

使用指定凭证登录

Signs in with the specified credentials

11.3 介绍页面对象模式

11.3 Introducing the Page Objects pattern

我们刚刚看到的类是页面对象模式的一个示例。页面对象是一个类,它模拟用户界面的特定部分,并提供一组更注重业务的方法供测试使用,从而使测试免受实际 HTML 页面的实现细节的影响。页面对象有两个主要角色:

The class we just looked at is an example of the Page Objects pattern. A Page Object is a class that models a specific part of the user interface and that presents a set of more business-focused methods for tests to use, sparing them from the implementation details of the actual HTML page. A Page Object has two main roles:

  • 它将页面的技术实现与测试隔离开来,使得测试代码更简单、更易于维护。

  • It isolates the technical implementation of the page from the tests, making the test code simpler and easier to maintain.

  • 它将与页面(或页面组件)交互的代码集中起来,以便当修改网页时,只需在一个地方更新测试代码。

  • It centralizes the code that interacts with a page (or a Page Component) so that when the web page is modified, the test code only needs to be updated in one place.

结构良好的自动验收测试套件通常至少由三层组成:

A well-structured automated acceptance test suite is typically made up of at least three layers:

  • 业务规则层描述预期的业务结果。

  • The Business Rules layer describes the expected business outcomes.

  • 业务流程层涉及用户通过应用程序的旅程。

  • The Business Flow layer relates the user’s journey through the application.

  • 交互层直接与系统交互。

  • The Interaction layer interacts directly with the system.

页面对象属于交互层(见图11.1),为业务流程层提供业务友好的服务,并通过WebDriver API实现与网页的交互。

Page Objects belong in the interaction layer (see figure 11.1). They provide business-friendly services to the Business Flow layer and implement the interactions with the web pages using the WebDriver API.

图11.1 页面对象在交互层发挥作用。

Figure 11.1 Page Objects play a role in the interaction layer.

页面对象到底起什么作用?让我们来一探究竟。

What exactly does a Page Object do? Let’s find out.

11.3.1 页面对象负责定位页面上的元素

11.3.1 Page Objects are responsible for locating elements on a page

页面对象的主要作用是了解网页上的内容以及如何与它们交互。换句话说,页面对象负责定位测试代码需要与之交互的元素。1页面对象知道要使用哪种定位器策略,并将它们存储在易于查找和需要时更新的位置。这避免了重复;如果 Web 元素发生变化,则只有一个地方需要修改定位器策略。

The primary role of a Page Object is to know where things are on a web page and how to interact with them. In other words, a Page Object is responsible for locating the elements your test code needs to interact with.1 The Page Object knows what locator strategy to use and stores them in a place that is easy to find and update when needed. This avoids duplication; if the web element changes, there is only one place that the locator strategy will need to be modified.

例如,在LoginPage课堂上我们在本章前面看到过,定位器策略由类顶部的常量值表示:

For example, in the LoginPage class we saw earlier in this chapter, the locator strategies are represented by constant values at the top of the class:

公共类登录页{
    私有静态最终 By EMAIL_FIELD = By.id("email");            
    private static final By PASSWORD_FIELD = By.id("密码");        
    私有静态最终 By LOGIN_BUTTON = By.id("login-button");
  ...   
public class LoginPage {
    private static final By EMAIL_FIELD = By.id("email");            
    private static final By PASSWORD_FIELD = By.id("password");        
    private static final By LOGIN_BUTTON = By.id("login-button");
  ...   

有时,定位器逻辑可能更复杂一些。例如,飞行常客应用程序的菜单栏几乎出现在每个屏幕的顶部(见图 11.2)。

Sometimes, locator logic can be a little more sophisticated. For example, the Frequent Flyer application has a menu bar that appears at the top of almost every screen (see figure 11.2).

图 11.2 常旅客菜单栏

Figure 11.2 The Frequent Flyer menu bar

我们可以选择将这些定位器分组到一个类中,每个按钮一个定位器,如下所示:

We could choose to group the locators for these in a single class, with a locator for each button, like this:

公共类菜单栏{
 
    private static final 来自 BOOK_FLIGHTS_BUTTON                 
        = By.xpath(“//button[contains(.,'预订航班')]”);     
    private static final 来自 MY_BOOKINGS_BUTTON                  
        = By.xpath(“//button[contains(.,'我的预订')]”);      
    private static final 来自 MY_ACCOUNT_BUTTON                   
        = By.xpath(“//button[contains(.,'我的账户')]”);       ​​❶
    private static final 来自                      LOGOUT_BUTTON❶
        = By.xpath(“//按钮[contains(.,'注销')]”);           ​​❶
    ...
public class MenuBar {
 
    private static final By BOOK_FLIGHTS_BUTTON                
        = By.xpath("//button[contains(.,'Book Flights')]");    
    private static final By MY_BOOKINGS_BUTTON                 
        = By.xpath("//button[contains(.,'My Bookings')]");     
    private static final By MY_ACCOUNT_BUTTON                  
        = By.xpath("//button[contains(.,'My Account')]");      
    private static final By LOGOUT_BUTTON                      
        = By.xpath("//button[contains(.,'Logout')]");          
    ...

我们为菜单栏中的每个按钮定义不同的 XPath 定位器。

We define a different XPath locator for each of the buttons in the menu bar.

但是每个链接的 HTML 结构和相应的 XPath 定位器非常相似,因此这感觉像是重复的工作。更灵活的方法可能是在所有按钮上采用单一的定位器策略,并使按钮的名称可参数化。使用此策略,完整的 Page Object 类可能看起来像这样:

But the HTML structures and the corresponding XPath locators for each link are very similar, so this feels like duplicated work. A more flexible approach might be to have a single locator strategy across all the buttons, and just make the name of the button parameterizable. Using this strategy, the complete Page Object class might look something like this:

公共类菜单栏{
 
    私人 WebDriver 驱动程序;
 
    公共菜单栏(WebDriver 驱动程序){ 
        这个.司机=司机; 
    }
 
    私有静态按钮带标签(字符串标签){            
        返回 By.xpath("//按钮[contains(.,'" + label + "')]");       
    }                        
    公共无效navigationToBookFlights(){
        driver.findElement(buttonWithLabel("预订航班")).click();    
    }
 
    公共无效navigationToMyBookings(){
        driver.findElement(buttonWithLabel("我的预订")).click();     
    }
 
    公共无效导航到我的帐户(){
        driver.findElement(buttonWithLabel("我的账户")).click();      
    }
 
    公共无效注销(){
        driver.findElement(buttonWithLabel("注销")).click();          
    }
}
public class MenuBar {
 
    private WebDriver driver;
 
    public MenuBar(WebDriver driver) { 
        this.driver = driver; 
    }
 
    private static By buttonWithLabel(String label) {            
        return By.xpath("//button[contains(.,'" + label + "')]");      
    }                        
    public void navigateToBookFlights() {
        driver.findElement(buttonWithLabel("Book Flights")).click();   
    }
 
    public void navigateToMyBookings() {
        driver.findElement(buttonWithLabel("My Bookings")).click();    
    }
 
    public void navigateToMyAccount() {
        driver.findElement(buttonWithLabel("My Account")).click();     
    }
 
    public void logout() {
        driver.findElement(buttonWithLabel("Logout")).click();         
    }
}

我们定义了一个可以用于任何按钮的通用 XPath 定位器,以及一种根据按钮的文本标签查找特定定位器的方法。

We define a generic XPath locator that can work for any of the buttons and a method to find the specific locator based on the text label of a button.

我们使用此方法来定位菜单栏中的每个按钮并进行交互。

We use this method to locate and interact with each of the buttons in the menu bar.

再次强调,位置逻辑隐藏在类本身内部,任何修改只需在一个类中完成地方。

Once again, the location logic is hidden inside the class itself, and any modifications only need to be done in one place.

11.3.2 页面对象表示页面上的对象,而不是整个页面

11.3.2 Page Objects represent objects on a page, not an entire page

尽管顾名思义,页面对象通常不代表整个页面。现代 Web 应用程序通常过于丰富和复杂,无法用单个对象来表示。只需考虑一下您通常在页面上看到的所有内容:菜单栏、导航元素、搜索框、搜索结果等等。如果您将所有这些东西放在同一个类中,代码很快就会变得庞大而笨拙,难以阅读和理解。

Despite their name, Page Objects do not typically represent an entire page. Modern web applications are usually too rich and complex to be represented as a single object. Just think about all the things that you typically see on a page: menu bars, navigation elements, search boxes, search results, and so forth. If you put all these things in the same class, the code would quickly become large and unwieldy, hard to read, and hard to understand.

更准确的术语是页面组件对象,因为它们代表页面上的一些可重复使用的组件。Martin Fowler 在其 2013 年关于页面对象的开创性文章中这样说道:“尽管有‘页面’对象这个术语,但这些对象通常不应为每个页面构建,而应为页面上的重要元素构建”( https://martinfowler.com/bliki/PageObject.xhtml )。例如,您可以使用页面对象来表示出现在每个屏幕上的主菜单栏,或者如果它出现在多个位置,则表示特色目的地列表(见图 11.3)。

A more accurate term is Page Component Objects because they represent some reusable component on the page. Martin Fowler, in his seminal article on Page Objects from 2013, puts it like this: “Despite the term ‘page’ object, these objects shouldn’t usually be built for each page, but rather for the significant elements on a page” (https://martinfowler.com/bliki/PageObject.xhtml). For example, you might use a Page Object to represent the main menu bar that appears on every screen, or for the list of featured destinations if this appears in several places (see figure 11.3).

图 11.3 网页通常包含许多不同的组件。

Figure 11.3 Web pages often contain many distinct components.

11.3.3 页面对象告诉你页面的状态

11.3.3 Page Objects tell you about the state of a page

对象还可以以业务术语报告它们在页面上看到的内容,隐藏这些信息的定位细节。这样既可以隐藏 WebDriver 逻辑,又可以更轻松地表达测试意图。

Page Objects can also report what they see on a page in business terms, hiding the details of how this information is located. This both keeps WebDriver logic hidden from your test code and makes it easier to express the intent of your tests.

例如,在上一章中,我们通过检查屏幕右上角显示的电子邮件地址实现了“然后她应该被授予访问她账户的权限”这一步骤。代码如下:

For example, in the previous chapter we implemented the “Then she should be given access to her account” step by checking the email address displayed in the top right corner of the screen. The code looked like this:

@Then(“他/她应该被授予访问他/她帐户的权限”)
公共无效应该被授予访问帐户的权限(){
    String currentUser = driver.findElement(By.id("current-user"))     
                               .getText();                             
    断言(currentUser).isEqualTo(frequentFlyer.email);            
}
 
@Then("he/she should be given access to his/her account")
public void shouldBeGivenAccessToTheAccount() {
    String currentUser = driver.findElement(By.id("current-user"))    
                               .getText();                            
    assertThat(currentUser).isEqualTo(frequentFlyer.email);           
}
 

在页面上找到当前用户描述

Locates the current user description on the page

获取该字段的文本内容

Fetches the text content of this field

将其与预期的当前用户的电子邮件地址进行比较

Compares it with the expected current user’s email address

在这里,我们直接与 WebDriver API 交互并同时执行多项操作:定位元素、获取文本并将其与电子邮件值进行比较。将所有这些操作放在一起会使阅读变得更加困难。

Here we are interacting directly with the WebDriver API and doing many things at once: locating the element, fetching the text, and comparing it to an email value. Lumping all this together makes it harder to read.

这也使得代码更难维护;即使在这个简单的例子中,也有许多原因需要更改此代码。例如,当前用户字段的定位器可能会更改,或者我们可能会切换到显示用户的名字,并带有友好的欢迎消息,如“欢迎,艾米!”,而不是无聊的电子邮件地址。第二种情况是业务逻辑的变化,这应该反映在步骤定义代码中,但第一种情况是关于我们如何从屏幕获取用户描述的实现细节;如果它发生变化,我们的步骤定义代码不应受到影响。

It also makes the code harder to maintain; even in this simple example, there are many reasons this code might need to change. For example, the locator for the current user field might change, or we might switch to displaying the user’s first name with a friendly welcome message like “Welcome, Amy!” instead of a boring email address. The second case is a change in business logic, which should be reflected in the step definition code, but the first is an implementation detail about how we get the user description from the screen; if it changes, our step definition code should not be affected.

当您使用页面对象模型(或本书推荐的其他模式)时,您永远不会直接与步骤定义方法中的 Web 元素交互。相反,您会使用更易读且以业务为中心的方法名称与其他类(如页面对象)交互,从而使测试代码的意图更加明显。

When you use the Page Object model (or the other patterns recommended in this book), you never interact directly with web elements inside your step definition methods. Instead, you interact with other classes, such as Page Objects, using more readable and business-centric method names that make the intent of your test code more obvious.

例如,使用页面对象模型策略,我们可以重构前面的代码示例,如下所示:

For example, using the Page Object model strategy, we could refactor the previous code sample like this:

公共类 CurrentUserPanel {
 
    私人 WebDriver 驱动程序;
    公共 CurrentUserPanel (WebDriver 驱动程序) {
        这个.司机=司机; 
    }    
 
    私有静态最终 By CURRENT_USER = By.id(“current-user”);     
 
    公共字符串标签(){
        返回 driver.findElement(CURRENT_USER).getText();            
    }
}
public class CurrentUserPanel {
 
    private WebDriver driver;
    public CurrentUserPanel (WebDriver driver) {
        this.driver = driver; 
    }    
 
    private static final By CURRENT_USER = By.id(“current-user”);    
 
    public String label() {
        return driver.findElement(CURRENT_USER).getText();           
    }
}

定义当前用户字段的定位策略。

Define the location strategy for the current user field.

获取并返回该字段的测试值。

Fetch and return the test value of this field.

我们的步骤定义代码现在看起来像这样:

Our step definition code would now look like this:

@Then(“他/她应该被授予访问他/她的帐户的权限”)
公共无效应该访问他的账户(){
    CurrentUserPanel currentUserPanel = new CurrentUserPanel(driver);       
    断言(currentUserPanel.label())。isEqualTo(frequentFlyer.email);    
}
@Then(“he/she should be given access to his/her account”)
public void shouldBeGivenAccessToHisAccount() {
    CurrentUserPanel currentUserPanel = new CurrentUserPanel(driver);      
    assertThat(currentUserPanel.label()).isEqualTo(frequentFlyer.email);   
}

创建一个新对象

Creates a new object

获取并返回该字段的测试值

Fetches and returns the test value of this field

注意到我们是如何返回字符串而不是暴露 WebElement 本身的吗?没有getCurrentUserElement()方法。页面对象不应暴露其封装的页面或组件的实现细节。页面对象方法应接受并返回简单类型,如字符串、日期、布尔值或特定于域的对象。它们不应暴露WebDriver或者WebElement

Notice how we are returning a string and not exposing the WebElement itself? There is no getCurrentUserElement() method. Page Objects should never expose implementation details about the page or component they’re encapsulating. Page Object methods should accept and return simple types such as strings, dates, Booleans, or domain-specific objects. They should never expose WebDriver or WebElement classes.

11.3.4 页面对象执行业务任务或模拟用户行为

11.3.4 Page Objects perform business tasks or simulate user behavior

对象隐藏了 WebDriver 的实现,并向世界呈现了更业务友好的外观。例如,LoginPage我们之前看到的并没有暴露每个 Web 元素供用户交互。我们通常不会编写这样的代码,即返回 WebElements 本身以供我们的代码交互:

Page Objects hide away the WebDriver implementation and present a more business-friendly façade to the world. For example, the LoginPage class we saw earlier does not expose each web element for users to interact with. We would not typically write code like this, where we return the WebElements themselves for our code to interact with:

登录页面.获取电子邮件字段()。发送密钥(frequentFlyer.email);
登录页面.获取密码字段()。发送密钥(frequentFlyer.密码);
登录页面.获取登录按钮().点击();
loginPage.getEmailField().sendKeys(frequentFlyer.email);
loginPage.getPasswordField().sendKeys(frequentFlyer.password);
loginPage.getLoginButton().click();

在某些情况下,我们可能会编写与这些元素交互的单独的方法,如下所示:

In some situations, we might write individual methods that interact with these elements, like this:

登录页面.输入邮箱地址(frequentFlyer.email);
登录页面.输入密码(frequentFlyer.密码);
登录页面.点击登录按钮();
loginPage.enterEmail(frequentFlyer.email);
loginPage.enterPassword(frequentFlyer.password);
loginPage.clickLoginButton();

但并非总是如此。例如,在这种情况下,登录是一个单一的业务操作;我们永远不会在不点击登录按钮的情况下单独输入电子邮件,因此,与其有三个单独的步骤,不如将它们全部分组为一个更适合业务的方法,这样会更方便,如下所示:

But not always. For example, in this case, logging in is a single business action; we would never enter an email alone, without clicking on the login button, so instead of having three separate steps, it would be much more convenient to group them all into a single more business-friendly method, like this:

    登录页面.signinWithCredentials(frequentFlyer.email, 
                                    常旅客会员密码);
    loginPage.signinWithCredentials(frequentFlyer.email, 
                                    frequentFlyer.password);

方法signinWithCredentials()专注于此操作的业务意图,并隐藏我们如何在方法内部与字段本身进行实际交互的细节。

The signinWithCredentials() method focuses on the business intent of this action and hides the details of how we actually interact with the fields themselves inside the method.

另一个例子是航班搜索表单(见图 11.4)。在这个表单中,FromToTravel class字段是必填字段,搜索按钮在填写之前是禁用的。

Another example could be the flight search form (see figure 11.4). On this form, the From, To, and Travel class fields are mandatory, and the search button is disabled until they are filled in.

图 11.4 Flying High 搜索表单

Figure 11.4 The Flying High search form

我们可以为该业务规则编写一个场景,如下(http://mng.bz/aMjz):

We could write a scenario for this business rule like this (http://mng.bz/aMjz):

背景  鉴于Amy 是 Frequency Flyer 的注册会员
  已使用有效的用户名和密码登录
 
规则:旅客必须至少提供出发地、目的地和旅行 
  场景模板:应突出显示缺少的必填字段
    她尝试搜索符合以下条件的航班
时      | 从 | 至 | 旅行舱位 | 
      | <从> | <至> | <旅行等级> | 
    那么搜查就不应该被允许
    并且<Missing Field> 字段应突出显示为缺失
    例如      | 从 | 至 | 旅行等级 | 缺失字段 |
      | | 香港 | 经济 | 来自 |
      | 悉尼 | | 经济 | 至 |
      | 悉尼 | 香港 | | 旅行舱位 |
Background:
  Given Amy is a registered Frequency Flyer member
  And she has logged on with a valid username and password
 
Rule: Travellers must provide at least departure, destination and travel 
 class
  Scenario Template: Missing mandatory fields should be highlighted
    When she tries to search for flights with the following criteria
      | From   | To   | Travel Class   | 
      | <From> | <To> | <Travel Class> | 
    Then the search should not be allowed
    And the <Missing Field> field should be highlighted as missing
    Examples:
      | From   | To        | Travel Class | Missing Field |
      |        | Hong Kong | Economy      | From          |
      | Sydney |           | Economy      | To            |
      | Sydney | Hong Kong |              | Travel class  |

现在假设我们想使用页面对象实现这个场景。我们可以定义一个SearchForm封装搜索表单及其字段。一种方法是为每个输入字段(例如,enterFrom()enterTo()selectTravelClass())提供一种方法,如下例所示:

Now suppose we want to implement this scenario using Page Objects. We might define a SearchForm class to encapsulate the search form and its fields. One approach is to have a method for each of these input fields (e.g., enterFrom(), enterTo(), and selectTravelClass()), like in the following example:

@When(“她/他尝试搜索符合以下条件的航班”)
公共无效搜索(航班搜索搜索){
    菜单栏.navigateToBookFlights();
    搜索表单.设置出发(搜索.来自());
    搜索表单.setArrival(搜索.to());
    searchForm.设置TravelClass(search.travelClass());
}
@When("she/he tries to search for flights with the following criteria")
public void searchBy(FlightSearch search) {
    menuBar.navigateToBookFlights();
    searchForm.setDeparture(search.from());
    searchForm.setArrival(search.to());
    searchForm.setTravelClass(search.travelClass());
}

页面SearchForm对象可能看起来像这样:

The SearchForm Page Object might look something like this:

公共类搜索表单{
 
    私人 WebDriver 驱动程序;
    公共 SearchForm (WebDriver 驱动程序) { this.driver = driver; }
 
    公共 void setDeparture(字符串出发){
        司机.findElement(By.id("出发"))。sendKeys(出发);           
    }
 
    公共无效设置目的地(字符串目的地){
        驱动程序.findElement(By.id("目的地"))。sendKeys(目的地);       
    }
 
    私有的OptionWithLabel(字符串标签){
        返回 By.xpath("//mat-option[normalize-space(.)='" + label + "']"); 
    }
 
    公共无效设置旅行类别(TravelClass travelClass){
        driver.findElement(By.id("travel-class")).click();                    
        驱动程序.findElement(optionWithLabel(travelClass.getLabel()))。点​​击(); 
    }
}
public class SearchForm {
 
    private WebDriver driver;
    public SearchForm (WebDriver driver) { this.driver = driver; }
 
    public void setDeparture(String departure) {
        driver.findElement(By.id("departure")).sendKeys(departure);          
    }
 
    public void setDestination(String destination) {
        driver.findElement(By.id("destination")).sendKeys(destination);      
    }
 
    private By optionWithLabel(String label) {
        return By.xpath("//mat-option[normalize-space(.)='" + label + "']"); 
    }
 
    public void setTravelClass(TravelClass travelClass) {
        driver.findElement(By.id("travel-class")).click();                   
        driver.findElement(optionWithLabel(travelClass.getLabel())).click(); 
    }
}

在出发字段中输入一个值

Enters a value into the departure field

在目标字段中输入一个值

Enters a value into the destination field

查找具有给定文本的下拉条目

Finds the dropdown entry with a given text

点击下拉字段以显示下拉选项

Clicks on the dropdown field to make the dropdown options appear

点击我们想要的选项

Clicks on the option we want

这种方法提供了业务可读的信息表示,同时仍然隐藏了页面上实际输入值的实现细节:例如,出发和到达字段是文本字段,而旅行类是 JavaScript 下拉组件,这需要一些更复杂的 WebDriver 代码才能完成。所有这些细节都隐藏在可读且友好的方法后面。

This approach presents a business-readable representation of the information while still hiding the implementation details of how the values are actually entered on the page: the departure and arrival fields are text fields, for example, whereas the travel class is a JavaScript dropdown component, which involves some more complex WebDriver code to complete. All these details are hidden away behind readable and friendly methods.

或者,如果我们倾向于在搜索中始终输入所有这些条件(即使我们的负面测试中有些条件可能为空),我们也可以选择使用单一方法的更简洁的方法,例如enterSearchCriteria()方法如图所示:

Alternatively, if we prefer to always enter all these criteria in a search (even if some might be empty for our negative tests), we could also opt for a more concise approach using a single method, such as the enterSearchCriteria() method shown here:

    searchForm.enterSearchCriteria("悉尼", "伦敦", TravelClass.ECONOMY);
    searchForm.enterSearchCriteria("Sydney", "London", TravelClass.ECONOMY);

完整的步骤定义将类似于这:

The full step definition would then look something like this:

@When(“她/他尝试搜索符合以下条件的航班”)
公共无效搜索(航班搜索搜索){
    菜单栏.navigateToBookFlights();
    searchForm.enterSearchCriteria(搜索.from(), 
                                   搜索.to(), 
                                   搜索.travelClass());
}
@When("she/he tries to search for flights with the following criteria")
public void searchBy(FlightSearch search) {
    menuBar.navigateToBookFlights();
    searchForm.enterSearchCriteria(search.from(), 
                                   search.to(), 
                                   search.travelClass());
}

11.3.5 页面对象以业务术语呈现状态

11.3.5 Page Objects present state in business terms

对象还应将页面状态信息转换为业务术语。例如,在图 11.4 所示的屏幕中,当搜索表单中的某个字段未填写时,该字段将以红色突出显示,并且搜索按钮将被禁用。

Page Objects should also convert information about the state of the page into business terms. For example, in the screen shown in figure 11.4, when a field is not completed in the search form it is highlighted in red and the search button is disabled.

要检查搜索按钮是否被禁用,我们可以简单地检查 Web 元素的状态,如下所示:

To check that the search button is disabled, we can simply check the state of the web element like this:

公共布尔搜索已启用(){
    返回驱动程序.findElement(By.id(“search-button”))。isEnabled();
}
 
步骤定义方法可以简单如下:
 
@Then("不应允许该搜索")
公共无效搜索ShouldNotBeAllowed(){
    断言(searchForm.searchIsEnabled())。是False();
}
public boolean searchIsEnabled() {
    return driver.findElement(By.id("search-button")).isEnabled();
}
 
And the step definition method could simply be:
 
@Then("the search should not be allowed")
public void searchShouldNotBeAllowed() {
    assertThat(searchForm.searchIsEnabled()).isFalse();
}

请注意,如果检查搜索按钮状态所需的逻辑发生变化,我们的步骤定义方法将保持不变。

Notice how, if the logic needed to check the state of the search button changes, our step definition method will remain unchanged.

检查突出显示的字段标签有点棘手。我们可以检查 CSS 颜色属性,但这会将您的代码与图形设计师的想法联系起来。更好的方法是寻找一个将文本标签标识为不完整的 CSS 选择器。事实证明,不完整的字段带有mat-form-field-invalid(参见图 11.4 中的 HTML 代码片段)。

Checking for the highlighted field labels is a bit trickier. We could check the CSS color attribute, but this will tie your code to the whims of the graphic designer. A better approach is to look for a CSS selector that identifies the text labels as incomplete. As it turns out, incomplete fields are tagged with the mat-form-field-invalid class (see the HTML snippet in figure 11.4).

我们可以向SearchForm类添加一个方法获取与该选择器匹配的所有 Web 元素并提取每个元素的文本值:

We could add a method to the SearchForm class to fetch all the web elements matching this selector and extract the text value of each element:

公共列表<String> missingFields() {
    返回 driver.findElements(By.cssSelector(".mat-form-field-invalid"))。   
            。溪流()
            .map(label -> label.getText().trim().replace(" *",""))。          
            .收集(Collectors.toList());                                   
}
public List<String> missingFields() {
    return driver.findElements(By.cssSelector(".mat-form-field-invalid")).  
            .stream()
            .map(label -> label.getText().trim().replace(" *","")).         
            .collect(Collectors.toList());                                  
}

查找所有以红色显示的匹配输入字段

Finds all the matching input fields that are displayed in red

提取字段标签并删除尾随的“*”字符

Extracts the field label and strip off the trailing “*” character

以字符串列表形式返回这些标签

Returns these labels as a list of strings

Page 对象向测试代码隐藏了这些细节,并将结果显示为易于使用的字段名称列表。使用此方法,我们可以使用From以下断言检查该字段是否被标记为缺失字段:

The Page Object hides these details from the test code and exposes the results as an easy-to-consume list of field names. Using this method, we could check that the From field is marked as a missing field with the following assertion:

    断言(searchForm.missingFields())。包含精确(“来自”);
    assertThat(searchForm.missingFields()).containsExactly(“From”);

再次注意 Page Object 方法如何隐藏获取此信息的实现细节,并简单地以易于使用的方式返回结果格式。

Notice once again how the Page Object method hides the implementation details of how this information is obtained and simply returns the results in an easy-to-consume format.

11.3.6 页面对象隐藏等待条件和其他附带的实现细节

11.3.6 Page Objects hide wait conditions and other incidental implementation details

常常在我们的测试中,我们需要等待某些条件满足后才能继续。我们之前看到了一些可以做到这一点的方法。但诸如等待条件之类的细节永远不应该直接出现在我们的测试中;它们很少与测试演示的业务逻辑相关,只会给测试代码增加噪音。

Oftentimes in our tests we need to wait for certain conditions to be met before continuing. We saw some ways you can do this earlier. But details such as wait conditions should never appear directly in our tests; they are rarely relevant to the business logic that a test demonstrates and just add noise to the test code.

页面对象方法是隐藏这些实现细节的好地方。例如,当我们在 Flying High 应用中执行搜索时,在检索航班时会出现一个旋转器(见图 11.5)。

Page Object methods are a great place to hide away these implementation details. For example, when we perform a search in our Flying High app, a spinner appears while the flights are retrieved (see figure 11.5).

图 11.5 搜索航班时 Flying High 应用程序中出现的旋转器

Figure 11.5 A spinner appearing on the Flying High app when you search for flights

现在想象一下我们需要自动化以下场景:

Now imagine we need to automate the following scenario:

规则:旅客可以按出发地、目的地和旅行等级进行搜索
  场景概述:按旅行等级搜索航班
    她搜索符合以下条件的航班
时      | 从 | 至 | 旅行舱位 |
      | <从> | <至> | <旅行等级> |
    那么回程航班应该符合旅行等级 <Travel Class>
    例如      | 从 | 至 | 旅行舱位 |
      | 悉尼 | 香港 | 经济 |
      | 伦敦 | 纽约 | 高级经济舱 |
      | 首尔 | 香港 | 商务 |
Rule: Travelers can search by departure, destination and travel class
  Scenario Outline: Searching for flights by travel class
    When she searches for flights with the following criteria
      | From   | To   | Travel Class   |
      | <From> | <To> | <Travel Class> |
    Then the returned flights should match the travel class <Travel Class>
    Examples:
      | From   | To        | Travel Class    |
      | Sydney | Hong Kong | Economy         |
      | London | New York  | Premium Economy |
      | Seoul  | Hong Kong | Business        |

在第一步中,我们需要等待旋转器消失,然后才能检查实际的搜索结果。但这是一个实现细节;也许明天搜索结果可能会以不同的方式返回,没有旋转器,但这不应该影响我们的场景或我们的步骤定义代码。

During the first step, we need to wait for the spinner to disappear before we can check the actual search results. But this is an implementation detail; perhaps tomorrow the search results might be returned differently, without the spinner, but this should not affect our scenario or our step definition code.

因此,我们将如何处理微调器的交互细节隐藏在 Page Object 方法中。例如,要执行实际搜索,我们可以扩展SearchForm该类使用submitSearch()方法,单击“搜索”按钮并等待微调器消失:

So, we hide the interaction details about how we deal with the spinner inside a Page Object method. For example, to perform the actual search, we could extend the SearchForm class with a submitSearch() method, which clicks on the Search button and waits for the spinner to disappear:

私有静态最终 By SEARCH_BUTTON = By.id("search-button");           
私有静态最终 By SPINNER = By.cssSelector(".block-ui-spinner");    
 
公共无效提交搜索(){
    驱动程序.findElement(SEARCH_BUTTON)。点击();                            
    等待 <WebDriver> wait = new FluentWait<>(驱动程序)
            .withTimeout(持续时间.ofSeconds(10))
            .pollingEvery(持续时间.ofMillis(100));
    等待.直到(invisibilityOfElementLocated(SPINNER));                    
}
private static final By SEARCH_BUTTON = By.id("search-button");          
private static final By SPINNER = By.cssSelector(".block-ui-spinner");   
 
public void submitSearch() {
    driver.findElement(SEARCH_BUTTON).click();                           
    Wait<WebDriver> wait = new FluentWait<>(driver)
            .withTimeout(Duration.ofSeconds(10))
            .pollingEvery(Duration.ofMillis(100));
    wait.until(invisibilityOfElementLocated(SPINNER));                   
}

如何找到搜索按钮

How to locate the search button

如何定位旋转器

How to locate the spinner

点击搜索按钮

Clicks on the search button

等待旋转器消失

Waits for the spinner to disappear

然后我们可以在步骤定义代码中调用此方法,而不需要知道有关微调器的任何信息:

We can then call this method in the step definition code without needing to know anything about the spinner:

@When(“她/他搜索符合以下条件的航班”)
公共无效执行搜索(FlightSearch搜索){
    这个.搜索标准 = 搜索;
    菜单栏.navigateToBookFlights();
    searchForm.enterSearchCriteria(搜索.from(), 
                                   搜索.to(), 
                                   搜索.travelClass());
    搜索表单.提交搜索();
}
@When("she/he searches for flights with the following criteria")
public void performSearch(FlightSearch search) {
    this.searchCriteria = search;
    menuBar.navigateToBookFlights();
    searchForm.enterSearchCriteria(search.from(), 
                                   search.to(), 
                                   search.travelClass());
    searchForm.submitSearch();
}

这样,我们的步骤定义代码就可以专注于与此相关的用户交互步。

This way, our step definition code can focus exclusively on the user interactions involved with this step.

11.3.7 页面对象不包含断言

11.3.7 Page Objects do not contain assertions

在上一个示例中,我们看到了 Page Object 如何返回无效字段列表,以便测试代码可以断言这些值是否是预期值。这是使用 Page Object 的典型方式:它们报告页面的状态,但不判断该状态是否符合预期。

In the previous example, we saw how our Page Object returns a list of invalid fields so that the test code can assert whether these values were the expected ones. This is the typical way you use Page Objects: they report the state of a page but don’t make judgment calls about whether this state is as expected.

换句话说,您的页面对象不应包含断言。将断言逻辑隐藏在页面对象代码中会使页面对象变得更大、更复杂,也更难读取测试实际测试的内容。

In other words, your Page Objects should not contain assertions. Burying assertion logic inside Page Object code makes the Page Objects larger and more complex and also makes it harder to read what a test is actually testing.

例如,在我们之前看到的代码中,我们使用了该label()方法CurrentUserPanel页面对象中返回当前连接用户的电子邮件:

For example, in the code we saw earlier, we used the label() method in the CurrentUserPanel Page Object to return the email of the currently connected user:

@Then(“他/她应该被授予访问他/她帐户的权限”)
公共无效应该访问他的账户(){
    CurrentUserPanel currentUserPanel = new CurrentUserPanel(驱动程序);    
    断言(currentUserPanel.label())。是否等于(frequentFlyer.email);    
}
@Then("he/she should be given access to his/her account")
public void shouldBeGivenAccessToHisAccount() {
    CurrentUserPanel currentUserPanel = new CurrentUserPanel(driver);    
    assertThat(currentUserPanel.label()).isEqualTo(frequentFlyer.email);    
}

在这里,Page Object 负责返回当前显示的用户 ID。关注点分离很明确:步骤定义代码描述了我们期望的内容(以断言的形式),而 Page Object 告诉我们它在屏幕上看到了什么。

Here, the Page Object is responsible for returning the currently displayed user ID. The separation of concerns is clear: the step definition code describes what we expect (in the form of an assertion), and the Page Object tells us what it sees on the screen.

我们永远不会将此断言逻辑包含在页面对象本身中。例如,以下代码将破坏我们试图通过将 UI 交互代码与测试逻辑分离来强制执行的关注点分离规则:

We would never include this assertion logic inside the Page Object itself. For example, the following code would break the separation of concerns rules we are trying to enforce by separating the UI interaction code from our test logic:

public void checkThatUserEmailDisplayedIs(String expectedEmail) {     
    断言(driver.findElement(CURRENT_USER).getText())            
              .isEqualTo(预期电子邮件);                              
}
public void checkThatUserEmailDisplayedIs(String expectedEmail) {    
    assertThat(driver.findElement(CURRENT_USER).getText())           
              .isEqualTo(expectedEmail);                             
}

反模式——我们违反了页面对象和测试代码之间的关注点分离。

Anti-pattern—we violate the separation of concerns between Page Objects and test code.

这会给我们的 Page Object 类增加不必要的复杂性(想象一下,如果我们需要为我们需要做的每一个检查添加断言方法!)并且使我们更难知道每个测试中正在测试什么。

This would add unnecessary complexity to our Page Object classes (imagine if we needed to add assertion methods for every check we needed to do!) and makes it harder to know what is being tested in each test.

现在我们对页面对象应该(和不应该)做什么有了更好的了解,让我们看看 Selenium 为我们提供的一些功能,以支持编写页面对象容易地。

Now that we have a better idea of what a Page Object should (and shouldn’t) do, let’s look at some of the features Selenium gives us to support writing Page Objects more easily.

11.3.8 WebDriver 页面工厂和 @FindBy 注释

11.3.8 WebDriver Page Factories and the @FindBy annotation

虽然你可以从头开始编写自己的页面对象,每次交互调用 WebDriver 方法很快就会变得重复。幸运的是,WebDriver 确实为我们提供了另一种选择。

Although you can write your own Page Objects from the ground up, invoking the WebDriver methods for each interaction quickly becomes repetitive. Fortunately, WebDriver does give us another option.

PageFactory这种支持以课程的形式出现@FindBy注释。这些结合起来可以更有效地定位网络元素。

This support comes in the form of the PageFactory class and the @FindBy annotation. Together, these can be used to locate web elements much more efficiently.

注释@FindBy允许您指定如何定位页面对象需要使用的不同 Web 元素。PageFactory知道如何扫描您的类以查找带注释的 Web 元素并对其进行配置,以便无论何时与它们交互,都可以使用正确的定位器策略动态检索它们。

The @FindBy annotation lets you specify how you want to locate the different web elements your Page Object needs to use. And the PageFactory class knows how to scan your class for annotated web elements and configure them so that they are retrieved on the fly using the right locator strategy, whenever you interact with them.

使用这种方法,你可以重写LoginPage页面对象我们之前看到过这样的open()方法(为简单起见已被排除):

Using this approach, you could rewrite the LoginPage Page Object we saw earlier like this (the open() method has been excluded for simplicity):

公共类登录页{
    @FindBy(id =“email”)                                    
    WebElement emailField;                                 
 
    @FindBy(id =“密码”)                                 
    WebElement 密码字段;                              
 
    @FindBy(id =“login-button”)                             
    WebElement 登录按钮;                                
 
    公共登录表单(WebDriver 驱动程序){
        PageFactory.initElements(驱动程序,此);            
    }
 
    public void signinWithCredentials(String email,String password){
        emailField.sendKeys(email);                        
        passwordField.sendKeys(密码);                  
        登录按钮.click();                               
    }
}
public class LoginPage {
    @FindBy(id="email")                                   
    WebElement emailField;                                
 
    @FindBy(id="password")                                
    WebElement passwordField;                             
 
    @FindBy(id="login-button")                            
    WebElement loginButton;                               
 
    public LoginForm(WebDriver driver) {
        PageFactory.initElements(driver, this);           
    }
 
    public void signinWithCredentials(String email, String password) {
        emailField.sendKeys(email);                       
        passwordField.sendKeys(password);                 
        loginButton.click();                              
    }
}

查找电子邮件字段

Looks up the email field

查找密码字段

Looks up the password field

查找登录字段

Looks up the login field

初始化邮箱和密码字段

Initializes the email and password fields

在使用网页元素之前先查看它们

Looks up the web elements on the web page before using them

注释@FindBy告诉 WebDriver 如何查找WebElement字段。当你以这种方式标记字段时,你可以使用PageFactory.initElements()方法在构造函数中为您实例化这些字段。每次使用这些字段时,WebDriver 都会执行相当于driver.findElement(By...)调用将它们绑定到网页上的相应元素。@FindBy注释支持您使用时可用的所有不同选择器方法driver.findElement(By...)(见表 11.1)。

The @FindBy annotation tells WebDriver how to look up a WebElement field. When you mark fields this way, you can use the PageFactory.initElements() method in the constructor to instantiate these fields for you. Each time you use these fields, WebDriver performs the equivalent of a driver.findElement(By...) call to bind them to the corresponding element on the web page. The @FindBy annotation supports all of the different selector methods available when you use driver.findElement(By...) (see table 11.1).

表 11.1 使用@FindBy注解的不同方法

Table 11.1 Different ways to use the @FindBy annotation

@FindBy表达

@FindBy expression

描述

Description

@FindBy(id="welcome-message")

@FindBy(id="welcome-message")

通过 ID 查找

Find by ID

@FindBy(name="email")

@FindBy(name="email")

按名称查找

Find by name

@FindBy(className="typeahead")

@FindBy(className="typeahead")

通过 CSS 类名查找

Find by CSS class name

@FindBy(css=".typeahead li")

@FindBy(css=".typeahead li")

通过 CSS 选择器查找

Find by CSS selector

@FindBy(linkText="Book")

@FindBy(linkText="Book")

通过链接文本查找

Find by link text

@FindBy(partialLinkText="Book")

@FindBy(partialLinkText="Book")

通过部分链接文本查找

Find by partial link text

@FindBy(tagName="h2")

@FindBy(tagName="h2")

通过 HTML 标签查找

Find by HTML tag

@FindBy(xpath="//span[.='Singapore']")

@FindBy(xpath="//span[.='Singapore']")

通过 XPath 表达式查找

Find by XPath expression

如果页面对象中的 WebElement 字段的名称与相应 HTML 元素的名称或 ID 匹配,则可以@FindBy完全跳过注释:

If the name of the WebElement fields in your Page Object matches either the name or ID of the corresponding HTML element, you can skip the @FindBy annotation entirely:

公共类登录表单{
    WebElement 电子邮件;          
    WebElement 密码;       
    
    @FindBy(id="登录按钮")
    Web元素登录按钮;
 
    ...
public class LoginForm {
    WebElement email;         
    WebElement password;      
    
    @FindBy(id="login-button")
    WebElement loginButton;
 
    ...

这里不需要@FindBy注解。

You don’t need the @FindBy annotation here.

在这种情况下,WebDriver 会自动实例化电子邮件和密码字段。例如,电子邮件字段相当于首先尝试@FindBy(id="email"),如果失败@FindBy(name="email")

In this case, WebDriver automatically instantiates the email and password fields. The email field, for example, is the equivalent of first trying @FindBy(id="email"), and if that fails, @FindBy(name="email").

11.3.9 查找集合

11.3.9 Finding collections

@FindBy注释不仅限于单个字段;您还可以使用此符号来检索 Web 元素集合。例如,在我们之前看到的一个场景中,我们需要检查所有返回的航班是否符合某些条件(见图 11.6):

The @FindBy annotation isn’t limited to individual fields; you can also use this notation to retrieve collections of web elements. For example, in one of the scenarios we saw earlier, we need to check that all the returned flights match certain criteria (see figure 11.6):

规则:旅客可以按出发地、目的地和旅行等级进行搜索
  场景概述:按旅行等级搜索航班
    她搜索符合以下条件的航班
时      | 从 | 至 | 旅行舱位 |
      | <从> | <至> | <旅行等级> |
    那么回程航班应该符合旅行等级 <Travel Class>
    例如      | 从 | 至 | 旅行舱位 |
      | 悉尼 | 香港 | 经济 |
      | 伦敦 | 纽约 | 高级经济舱 |
      | 首尔 | 香港 | 商务 |
Rule: Travelers can search by departure, destination and travel class
  Scenario Outline: Searching for flights by travel class
    When she searches for flights with the following criteria
      | From   | To   | Travel Class   |
      | <From> | <To> | <Travel Class> |
    Then the returned flights should match the travel class <Travel Class>
    Examples:
      | From   | To        | Travel Class    |
      | Sydney | Hong Kong | Economy         |
      | London | New York  | Premium Economy |
      | Seoul  | Hong Kong | Business        |

图 11.6 匹配航班列表

Figure 11.6 The list of matching flights

为了实现这个逻辑,我们可以定义一个MatchingFlightsList页面对象,负责以我们可以在步骤定义中使用的格式返回匹配航班的列表。在这个类中,我们可以使用@FindBy注释来检索与匹配航班相对应的 Web 元素列表:

To implement this logic, we could define a MatchingFlightsList Page Object, responsible for returning a list of matching flights in a format that we can use in our step definitions. In this class, we could use the @FindBy annotation to retrieve the list of web elements that correspond to the matching flights:

@FindBy(css = “ .flight-container .card”)
私人列表<WebElement> matchingFlights;
@FindBy(css = ".flight-container .card")
private List<WebElement> matchingFlights;

但是我们需要以业务友好的格式返回这些结果,而不是以 WebElements 列表的形式,以便我们在步骤定义代码中使用。例如,我们可以使用 Java 记录(Java 14 或更高版本中提供的语言功能)结构对搜索结果中的条目进行建模,如下所示:

But we need to return these results not as a list of WebElements, but in a business-friendly format we can use in our step definition code. For example, we might model the entries in the search results using a Java record (a language feature available in Java 14 or higher) structure like this:

  公共记录 MatchingFlight(字符串出发, 
                               字符串目的地, 
                               旅行等级 travelClass) {}
  public record MatchingFlight(String departure, 
                               String destination, 
                               TravelClass travelClass) {}

这样我们就可以将 Web 元素转换为 MatchingFlight 对象。使用 Java 8 流,代码可能如下所示:

This way we could convert the web elements into MatchingFlight objects. Using Java 8 streams, the code might look like this:

matchingFlights.stream()
        .map(元素->新MatchingFlight(
                元素.findElement(By.cssSelector(".departure")).getText(),
                元素.findElement(By.cssSelector(“.destination”))。getText(),
                TravelClass.带标签(
                        元素.findElement(
                                通过.cssSelector(“.travel-class”)
                        )获取文本()
        ))收集(Collectors.toList());
matchingFlights.stream()
        .map(element -> new MatchingFlight(
                element.findElement(By.cssSelector(".departure")).getText(),
                element.findElement(By.cssSelector(".destination")).getText(),
                TravelClass.withLabel(
                        element.findElement(
                                By.cssSelector(".travel-class")
                        ).getText())
        )).collect(Collectors.toList());

经过一些重构后,完整的类现在可能看起来像这样:

After a little refactoring, the full class might now look like this:

公共类 MatchingFlightsList {
 
    私人 WebDriver 驱动程序;
 
    公共 MatchingFlightsList(WebDriver 驱动程序){
        这个.司机=司机;
        PageFactory.initElements(驱动程序,这个);
    }
 
    @FindBy(css =“ .flight-container .card”)                                 
    私有列表<WebElement> matchingFlights;                                
 
    私有最终静态 By DEPARTURE = By.cssSelector(".departure");        
    私有最终静态 By DESTINATION = By.cssSelector(“.destination”);    
    私有最终静态 By TRAVEL_CLASS = By.cssSelector(".travel-class"); 
 
    公共列表<MatchingFlight> matchingFlights() {
        返回 matchingFlights.stream()                                      
                .map(元素 -> new MatchingFlight(                          
                        element.findElement(DEPARTURE).getText(),            
                        element.findElement(DESTINATION).getText(),          
                        TravelClass.withLabel(                               
                            element.findElement(TRAVEL_CLASS).getText()      
                ))收集(Collectors.toList());            
    }
}
public class MatchingFlightsList {
 
    private WebDriver driver;
 
    public MatchingFlightsList(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }
 
    @FindBy(css = ".flight-container .card")                                
    private List<WebElement> matchingFlights;                               
 
    private final static By DEPARTURE = By.cssSelector(".departure");       
    private final static By DESTINATION = By.cssSelector(".destination");   
    private final static By TRAVEL_CLASS = By.cssSelector(".travel-class"); 
 
    public List<MatchingFlight> matchingFlights() {
        return matchingFlights.stream()                                     
                .map(element -> new MatchingFlight(                         
                        element.findElement(DEPARTURE).getText(),           
                        element.findElement(DESTINATION).getText(),         
                        TravelClass.withLabel(                              
                            element.findElement(TRAVEL_CLASS).getText()     
                        )
                )).collect(Collectors.toList());            
    }
}

查找每个航班结果的 Web 元素列表

Finds the list of web elements for each flight result

定义搜索表单中元素的定位器策略

Defines locator strategies for the elements within the search form

将匹配的航班列表转换为 MatchingFlight 记录

Converts the list of matching flights to MatchingFlight records

为每个部分创建一个新的 MatchingFlight

Creates a new MatchingFlight for each section

使用嵌套查询查找每个航班的出发地、目的地和旅行等级

Finds the departure, destination, and travel class for each flight using a nested query

拼图的最后一部分就是简单地检索MatchingFlight记录列表,并检查每条记录是否与预期的旅行相匹配班级:

The final piece of the puzzle is then simply to retrieve the list of MatchingFlight records and check that each one matches the expected travel class:

@ParameterType("经济舱|高级经济舱|商务舱")                         
公共 TravelClass travelClass (字符串值){                             
    返回 TravelClass.withLabel(值);                                 
}
 
@Then("返回的航班应符合旅行等级{travelClass}")
公共无效应该匹配TravelClass(TravelClass预期类){
    断言(matchingFlightsList.matchingFlights())                      
            .isNotEmpty()                                                  
            .allMatch(flight -> flight.travelClass() == expectedClass,     
                      “应该有旅行等级” + expectedClass     
    (英文):
}
@ParameterType("Economy|Premium Economy|Business")                        
public TravelClass travelClass(String value) {                            
    return TravelClass.withLabel(value);                                 
}
 
@Then("the returned flights should match the travel class {travelClass}")
public void shouldMatchTravelClass(TravelClass expectedClass) {
    assertThat(matchingFlightsList.matchingFlights())                     
            .isNotEmpty()                                                 
            .allMatch(flight -> flight.travelClass() == expectedClass,    
                      "should have a travel class of " + expectedClass    
    );
}

定义一个 Cucumber 参数类型,用于将旅行类别标签转换为相应的 Enum 值

Defines a Cucumber parameter type to convert the travel class label into the corresponding Enum value

获取当前显示的匹配航班

Fetches the matching flights currently displayed

至少应有一个匹配的航班。

There should be at least one matching flight.

所有航班均应属于预期的旅行舱位。

All of the flights should be in the expected travel class.

11.3.10 Serenity BDD 中的页面对象

11.3.10 Page Objects in Serenity BDD

在接下来的几章中,我们将使用 Serenity BDD 内置的 WebDriver 支持(http://serenity-bdd.info/)。我们将讨论的所有事情都离不开 Serenity BDD,但它确实允许我们用更少的样板和设置编写更多的自动化代码。

In the code samples in the next few chapters, we will use the WebDriver support built into Serenity BDD (http://serenity-bdd.info/). There’s nothing we will discuss that you can’t do without using Serenity BDD, but it does allow us to write more automation code with less boilerplate and setup.

Serenity BDD 提供了许多快捷方式,使页面对象编写起来更轻松、更方便。例如,Serenity BDD 负责设置和关闭 WebDriver 实例,因此您无需亲自执行这些操作。

Serenity BDD provides a number of shortcuts that make Page Objects easier and more convenient to write. For example, Serenity BDD handles setting up and shutting your WebDriver instance, so you don’t need to do that yourself.

Serenity BDD 还增强了 WebDriver API,特别是通过PageObjectWebElementFacade课程。例如,type()方法扩展sendKeys()方法WebElement由班级提供通过确保字段已准备就绪并在发送键值之前清除它。

Serenity BDD also enhances the WebDriver API, notably through the PageObject and WebElementFacade classes. For example, the type() method extends the sendKeys() method provided by the WebElement class by ensuring that the field is ready and clearing it before sending the key values.

页面LoginPage对象我们之前看到的可以使用 Serenity BDD 实现,如下所示:

The LoginPage Page Object we saw earlier could be implemented using Serenity BDD like this:

@DefaultUrl(“/login”)                                
公共类LoginPage扩展了PageObject{          
    @FindBy(id="电子邮件")
    WebElementFacade 电子邮件;                          
    
    @FindBy(id="密码")
    WebElementFacade 密码;
    
    @FindBy(id="登录按钮")
    WebElementFacade 登录按钮;
 
    公共无效signinWithCredentials(字符串emailValue, 
                                      字符串密码值){
        电子邮件.类型(电子邮件值);                      
        密码.类型(密码值);                
        登录按钮.点击();
    }
}
@DefaultUrl("/login")                               
public class LoginPage extends PageObject {         
    @FindBy(id="email")
    WebElementFacade email;                         
    
    @FindBy(id="password")
    WebElementFacade password;
    
    @FindBy(id="login-button")
    WebElementFacade loginButton;
 
    public void signinWithCredentials(String emailValue, 
                                      String passwordValue) {
        email.type(emailValue);                     
        password.type(passwordValue);               
        loginButton.click();
    }
}

定义打开此页面对象时要使用的相对或绝对 URL

Defines the relative or absolute URL to be used when this Page Object is opened

所有 Serenity BDD 页面对象都扩展了 PageObject 类。

All Serenity BDD Page Objects extend the PageObject class.

可选的 WebElementFacade 类增强了 WebDriver 的 WebElement 类。

The optional WebElementFacade class enhances WebDriver’s WebElement class.

定义一个 Cucumber 参数类型,用于将旅行类别标签转换为相应的 Enum 值

Defines a Cucumber parameter type to convert the travel class label into the corresponding Enum value

Serenity 还将为您实例化测试中的任何页面对象字段,因此您无需显式创建它们。我们的步骤定义代码可以这样写:

Serenity will also instantiate any Page Object fields in your tests for you, so you don’t need to explicitly create them. Our step definition code could be written like this:

登录页面 登录页面;      
 
@When("^s?he (?:logs|has login) on with a valid username and password$")
公共 void logsOnWithAValidUsernameAndPassword() {
    登录页面.打开();
    登录页面.signinWithCredentials(frequentFlyer.email, 
                                    常旅客会员密码);
}
LoginPage loginPage;     
 
@When("^s?he (?:logs|has logged) on with a valid username and password$")
public void logsOnWithAValidUsernameAndPassword() {
    loginPage.open();
    loginPage.signinWithCredentials(frequentFlyer.email, 
                                    frequentFlyer.password);
}

Serenity 将使用当前 WebDriver 实例配置的页面对象自动初始化此字段。

Serenity will automatically initialize this field with a Page Object configured with the current WebDriver instance.

您可以在 Serenity BDD 上阅读有关 Serenity BDD 的更多信息网站http://serenity-bdd.info/)。

You can read more about Serenity BDD on the Serenity BDD website (http://serenity-bdd.info/).

11.4 超越页面对象

11.4 Going beyond Page Objects

到目前为止,我们已经了解了如何使用页面对象通过将页面对象类和方法内的 WebDriver 代码与测试代码本身隔离开来,使我们的 UI 自动化代码更易于阅读和维护。

So far, we have seen how to use Page Objects to make our UI automation code easier to read and maintain by isolating WebDriver code inside Page Object classes and methods, away from the test code itself.

编写良好的页面对象比简单的测试脚本更易于维护,并且非常适合小型应用程序。但是,随着应用程序的增长,我们经常会遇到限制。例如,页面对象在定义上专注于用户界面;如果您只有页面对象,您可能会倾向于通过 UI 测试所有内容,从而错失通过使用更快、更可靠的 API 调用实施某些步骤来简化测试的机会。

Well-written Page Objects are much more maintainable than simple test scripts and work well for small applications. However, as an application grows, we often run into limitations. For example, a Page Object is by definition focused on the user interface; if all you have are Page Objects, there can be a temptation to test everything through the UI and miss the opportunity to streamline tests by implementing certain steps with faster and more reliable API calls.

页面对象也可能变得庞大而臃肿,将业务逻辑(例如“搜索航班”)与 UI 实现细节(“从下拉菜单中选择旅行类别”)混合在一起。许多人陷入编写页面对象的陷阱,这些页面对象紧密模拟并复制用户界面的结构,这会使页面对象类更加复杂和脆弱。

Page Objects can also become large and bloated, mixing business-focused logic (e.g., “search for flights”) with UI-implementation details (“select travel class from the dropdown menu”). Many folks fall into the trap of writing Page Objects that closely model and duplicate the structure of the user interface, which can make the Page Object classes more complicated and fragile.

避免这些问题的一个好方法是在步骤定义和交互层之间引入额外的抽象层。对于更大或更复杂的应用程序,这种方法有许多优点:

A good way to avoid these problems is to introduce an additional layer of abstraction between your step definitions and your interaction layer. This approach has a number of advantages for larger or more complex applications:

  • Cucumber 场景中以业务为中心的步骤(内容)与这些步骤如何实现的具体细节(方法)之间的区分更加清晰

  • A much cleaner separation between the business-focused steps in the Cucumber scenarios (the what), and the nitty-gritty details of how these steps are implemented (the how)

  • 在不同场景之间重用业务步骤的更多选项,而无需增加页面对象的复杂性

  • More options to reuse business steps between different scenarios, without having to add complexity to our Page Objects

我们有时将这种方法称为领域特定语言(DSL)),因为它以一种非常易读的方式对业务领域进行建模。我们将实现此层的类和对象称为操作(以将它们与页面对象区分开来)。让我们看看这种方法在实践中是如何工作的。

We sometimes call this approach a domain-specific language (DSL), because it models the business domain in a very readable way. We call the classes and objects that implement this layer actions (to distinguish them from Page Objects). Let’s see how this approach works in practice.

11.4.1 动作类

11.4.1 Action classes

早些时候在本章中,我们研究了一个简单的场景,描述了飞行常客会员如何使用他们的凭证连接到 Flying High 网站:

Earlier in the chapter we looked at a simple scenario describing how a Frequent Flyer member can connect to the Flying High site using their credentials:

示例:Amy 成功登录飞行常客计划应用程序
  鉴于Amy 是 Frequency Flyer 的注册会员
  她使用有效的用户名和密码登录
时  那么她应该被授予访问自己账户的权限
Example: Amy successfully logs on to the Frequent Flyer app
  Given Amy is a registered Frequency Flyer member
  When she logs on with a valid username and password
  Then she should be given access to her account

使用页面对象的步骤定义代码如下所示:

The step definition code using Page Objects looked something like this:

@When(“他/她使用有效的用户名和密码登录”)
公共 void logsOnWithAValidUsernameAndPassword() {
    登录页 loginPage = 新登录页 (驱动程序);
    登录页面.open();                                           
                                                       
    loginPage.signinWithCredentials(frequentFlyer.email,        
                                    常旅客.密码)     
     
}
@When("he/she logs on with a valid username and password")
public void logsOnWithAValidUsernameAndPassword() {
    LoginPage loginPage = new LoginPage(driver);
    loginPage.open();                                          
                                                       
    loginPage.signinWithCredentials(frequentFlyer.email,       
                                    frequentFlyer.password)    
     
}

打开登录页面

Opens the login page

使用一些有效凭证登录

Signs in with some valid credentials

阅读屏幕上显示的电子邮件并将其与预期电子邮件进行比较

Reads the email displayed on the screen and compares it with the expected one

此代码与用户界面实现紧密耦合。例如,在第一步中,我们明确打开一个页面并输入凭据。这本身没什么问题,但这意味着我们只能通过 UI 执行此步骤。如果我们想简化流程(例如通过 Cookie 提供身份验证详细信息,或使用其他身份验证机制),则需要重写整个步骤。现在想象一下,如果我们有一个类,其工作是从业务角度对登录过程进行建模 - 不是登录屏幕,而是整个登录过程,无论我们是通过 UI 还是使用其他机制登录。

This code is very tightly coupled to the user interface implementation. For example, in the first step, we explicitly open a page and enter the credentials. This is fine as it stands, but it means that we can only perform this step via the UI. If we wanted to streamline things (for example by providing our authentication details via a Cookie, or using some other authentication mechanism), we would need to rewrite the whole step. Now imagine if we had a class whose job it was to model the login process from a business perspective—not the login screen, but the login process as a whole, regardless of whether we were logging in via the UI or using some other mechanism.

我们可以Login上课知道如何验证特定飞行常客用户的身份:

We could have a Login class that knows how to authenticate as a given Frequent Flyer user:

@步骤             
登录login;        
 
@When("^s?he (?:logs|has login) on with a valid username and password$")
公共 void logsOnWithAValidUsernameAndPassword() {
    登录.作为(飞行常客);                        
}
@Steps             
Login login;       
 
@When("^s?he (?:logs|has logged) on with a valid username and password$")
public void logsOnWithAValidUsernameAndPassword() {
    login.as(frequentFlyer);                        
}

Serenity BDD 注释,可自动实例化此类以及任何嵌套的页面对象

A Serenity BDD annotation that automatically instantiates this class and any nested Page Objects

登录动作类代表登录的高级动作。

The Login Action class represents the high-level action of logging in.

班级Login将实现该as()方法并知道如何使用我们之前定义的 Page Object 类打开登录页面并输入正确的用户凭据。登录涉及两个步骤:打开登录页面,然后输入正确的凭据,因此我们可以实现Login该类像这样:

The Login class would implement the as() method and know how to open the login page and enter the correct user credentials, using the Page Object class we defined earlier. Logging in involves two steps: opening the login page and then entering the correct credentials, so we could implement the Login class like this:

公共类登录{
 
    登录表单 loginForm;                             
 
    @Step("以 {0} 身份登录")                            
    公共无效为(FrequentFlyer 常旅客){
        登录表单.打开();
        登录表单.输入凭据(frequentFlyer.email, 
                                   常旅客会员密码);
        登录表单.提交();
    }
}
public class Login {
 
    LoginForm loginForm;                            
 
    @Step("Login as {0}")                           
    public void as(FrequentFlyer frequentFlyer) {
        loginForm.open();
        loginForm.enterCredentials(frequentFlyer.email, 
                                   frequentFlyer.password);
        loginForm.submit();
    }
}

❶Page Object 字段将由 Serenity BDD 自动初始化。

The Page Object field will be automatically initialized by Serenity BDD.

一个 Serenity BDD 注解使得此方法调用作为一个步骤出现在测试报告中。“{0}”表示该方法的第一个参数,在本例中为常旅客。

A Serenity BDD annotation that makes this method call appears as a step in the test report. "{0}" represents the first parameter of the method, the frequent flyer in this case.

班级Login是 Action 类的一个示例,Action 类封装了特定的业务任务或操作。Action 类知道它们需要与哪些页面(或 API)交互,但不需要知道实现细节,例如如何在页面上定位特定元素页。

The Login class is an example of an Action class, a class that encapsulates a specific business task or action. Action classes know which pages (or APIs) they need to interact with but don’t need to know implementation details such as how to locate a particular element on a page.

11.4.2 查询类

11.4.2 Query classes

其他Action 类的类型,我们有时称之为查询类,专注于读取系统状态并以业务友好的术语将其返回给测试代码。前面我们看到了如何编写 Page Object 方法来执行此操作。步骤定义代码获取屏幕上显示的电子邮件地址并将其与当前用户的电子邮件地址进行比较:

Another type of Action class, that we sometimes call a Query class, focuses on reading the state of the system and returning it to the test code in business-friendly terms. Earlier we saw how to write Page Object methods to do this. The step definition code got the email address displayed on the screen and compared it to that of the current user:

@Then(“他/她应该被授予访问他/她帐户的权限”)
公共无效应该访问他的账户(){
    CurrentUserPanel currentUserPanel = new CurrentUserPanel(驱动程序);    
    断言(currentUserPanel.email())。是否等于(frequentFlyer.email);    
}
@Then("he/she should be given access to his/her account")
public void shouldBeGivenAccessToHisAccount() {
    CurrentUserPanel currentUserPanel = new CurrentUserPanel(driver);    
    assertThat(currentUserPanel.email()).isEqualTo(frequentFlyer.email);    
}

但这会给我们的 Page Object 类增加不必要的复杂性,尤其是当不同的测试需要从同一页面获取不同的信息时。它还将我们的测试类绑定到实现。例如,如果我们决定显示用户的名字而不是他们的电子邮件,会发生什么?该步骤的意图(“他/她应该被授予访问他/她帐户的权限”)不会有任何降低。但实施会中断。

But this can add unnecessary complexity to our Page Object classes, especially if different tests need to obtain different information from the same page. It also binds our test class to the implementation. For example, what happens if we decide to display the user’s first name instead of their email? The intent of the step (“he/she should be given access to his/her account”) wouldn’t be any less valid. But the implementation would break.

我们可以编写一个专用的查询类来负责了解当前用户是否与预期用户匹配,而不是将此方法添加到页面对象中。使用 Serenity BDD 支持类,我们可以编写如下查询类:

Rather than adding this method to a Page Object, we could write a dedicated query class responsible for knowing if the current user matches the expected one. Using the Serenity BDD support classes we could write a Query class like this:

public class CurrentUser 扩展了 UIInteractionSteps {                   
    公共布尔值 isConnectedAs(FrequentFlyer 常旅客){
        返回frequentFlyer.email.equals(textOf("#current-user"));     
    }
}
public class CurrentUser extends UIInteractionSteps {                  
    public boolean isConnectedAs(FrequentFlyer frequentFlyer) {
        return frequentFlyer.email.equals(textOf("#current-user"));    
    }
}

用于与用户界面交互的 Action 或 Query 类的 Serenity BDD 基类

A Serenity BDD base class used for Action or Query classes that interact with the user interface

将预期的电子邮件与当前显示的值进行比较。 textOf() 方法返回由给定定位器定位的元素的文本值。

Compare the expected email with the currently displayed value. The textOf() method returns the text value of an element located by a given locator.

现在我们的步骤定义只需要检查当前用户是否按预期连接,而不必知道这是如何实现的完毕:

Now our step definition just needs to check whether the current user is connected as the expected one, without having to know how this is done:

@Then(“他/她应该被授予访问他/她帐户的权限”)
公共无效应该访问他的账户(){
    assertTrue("当前用户应显示为 " + oftenFlyer,    
               currentUser.isConnectedAs(frequentFlyer));                 
}
@Then("he/she should be given access to his/her account")
public void shouldBeGivenAccessToHisAccount() {
    assertTrue("the current user should be shown as " + frequentFlyer,   
               currentUser.isConnectedAs(frequentFlyer));                
}

(可选)额外文本参数有助于更清楚地描述我们所主张的内容。

The (optional) extra text parameter helps describe what we are asserting more clearly.

检查当前用户是否与预期用户匹配

Checks that the current user matches the expected one

11.4.3 DSL 层和构建器

11.4.3 DSL layers and builders

造型Action 类中的业务流程为我们提供了很大的灵活性;它使我们能够编写极具表现力和可读性的应用程序代码,以模拟用户和系统的行为,从而使我们的测试代码更易于编写、更易于理解且维护成本更低。

Modeling our business flow in Action classes gives us a lot of flexibility; it makes it possible to write highly expressive and readable application code that models the behavior of our users and of the system, which makes our test code easier to write, easier to understand, and cheaper to maintain.

让我们看另一个例子来了解我们的意思。之前我们研究了以下搜索场景:

Let’s look at another example to see what we mean. Earlier we looked at the following search scenario:

场景概述:按旅行等级搜索航班
  她搜索符合以下条件的航班
时    | 从 | 至 | 旅行舱位 |
    | <从> | <至> | <旅行等级> |
  那么回程航班应该符合旅行等级 <Travel Class>
  例如    | 从 | 至 | 旅行舱位 |
    | 悉尼 | 香港 | 经济 |
    | 伦敦 | 纽约 | 高级经济舱 |
    | 首尔 | 香港 | 商务 |
Scenario Outline: Searching for flights by travel class
  When she searches for flights with the following criteria
    | From   | To   | Travel Class   |
    | <From> | <To> | <Travel Class> |
  Then the returned flights should match the travel class <Travel Class>
  Examples:
    | From   | To        | Travel Class    |
    | Sydney | Hong Kong | Economy         |
    | London | New York  | Premium Economy |
    | Seoul  | Hong Kong | Business        |

我们已经看到了使用页面对象实现此测试代码的几种方法,这些方法非常适合此特定场景。但是,假设我们有其他场景,例如往返或多站旅程?

We’ve seen a few ways to implement this test code using Page Objects, which work very well for this specific scenario. But suppose we had other scenarios, such as for a return trip or a multistop journey?

一种方法是使用流畅接口使我们的代码更加灵活和更具表现力。流畅接口是一种使用方法链来提高可读性的 API。如果我们使用一个,我们的步骤定义代码可能看起来像这样:

One approach would be to use a Fluent Interface to make our code more flexible and more expressive. A Fluent Interface is an API that uses method chaining to improve readability. Our step definition code might look something like this if we use one:

@When(“她/他搜索符合以下条件的航班”)
公共无效执行搜索(FlightSearch搜索){
    searchFlights.from(search.from())
            .to(搜索.to())
            .inTravelClass(搜索.travelClass())
            .and查看结果();
}
@When("she/he searches for flights with the following criteria")
public void performSearch(FlightSearch search) {
    searchFlights.from(search.from())
            .to(search.to())
            .inTravelClass(search.travelClass())
            .andViewResults();
}

例如,如果我们需要测试回程,我们可以轻松扩展 API 以满足这样的额外条件:

If we needed to test return trips, for example, we could easily extend the API to cater to the extra condition like this:

@When(“她/他搜索符合以下条件的返程航班”)
公共无效执行搜索(FlightSearch搜索){
    searchFlights.from(search.from())
            .to(搜索.to())
            .inTravelClass(搜索.travelClass())
            .withAReturnTrip()
            .and查看结果();
}
@When("she/he searches for return flights with the following criteria")
public void performSearch(FlightSearch search) {
    searchFlights.from(search.from())
            .to(search.to())
            .inTravelClass(search.travelClass())
            .withAReturnTrip()
            .andViewResults();
}

多站旅程可能如下所示:

And a multistop journey might look like this:

    searchFlights.from("伦敦")
            .to("纽约")
            .thenTo("洛杉矶")
            .inTravelClass(TravelClass.ECONOMY)
            .withAReturnTrip()
            .and查看结果();
    searchFlights.from("London")
            .to("New York")
            .thenTo("Los Angeles")
            .inTravelClass(TravelClass.ECONOMY)
            .withAReturnTrip()
            .andViewResults();

实现此类的方法有很多,编写 DSL 本身就是一个值得写一本书的主题。但是,通过使用我们现有的页面对象,我们可以根据需要为类的每个方法调用一个或多个页面对象:

There are many ways to implement a class like this, and writing DSLs is a topic worth a book in its own right. But by using our existing Page Objects, we could simply invoke one or more Page Objects as required for each method of the class:

公共类 SearchFlights 扩展了 UIInteractionSteps {
 
    @步骤
    导航导航;
    搜索表单 搜索表单;
 
    公共搜索航班出发地(字符串出发地){
        导航.到TheBookFlightsPage();
        searchForm.setDeparture(出发);
        返回这个;
    }
 
    公共搜索航班(字符串目的地){
        searchForm.设置目的地(目的地);
        返回这个;
    }
    公共搜索航班 inTravelClass(TravelClass travelClass) {
        searchForm.setTravelClass(travelClass);
        返回这个;
    }
 
    公共无效andViewResults(){
        搜索表单.提交搜索();
    }
}
public class SearchFlights extends UIInteractionSteps {
 
    @Steps
    Navigate navigate;
    SearchForm searchForm;
 
    public SearchFlights from(String departure) {
        navigate.toTheBookFlightsPage();
        searchForm.setDeparture(departure);
        return this;
    }
 
    public SearchFlights to(String destination) {
        searchForm.setDestination(destination);
        return this;
    }
    public SearchFlights inTravelClass(TravelClass travelClass) {
        searchForm.setTravelClass(travelClass);
        return this;
    }
 
    public void andViewResults() {
        searchForm.submitSearch();
    }
}

这是一个非常简单的流畅接口示例,这种编码风格允许我们在自己的代码中编写更灵活、更易读的代码。步骤定义。

This is a very simple example of a Fluent Interface, a coding style that allows us to write much more flexible and readable code in our step definitions.

概括

Summary

  • 您可以使用页面对象使 UI 场景的步骤定义代码更易于维护。

  • You can use Page Objects to make the step definition code for UI scenarios more maintainable.

  • 您可以使用 Action 类和 Fluent 接口将步骤定义代码与 WebDriver API 隔离。

  • You can use Action classes and Fluent Interfaces to isolate your step definition code from the WebDriver API.

在下一章中,我们将介绍一种更高级的编写步骤定义代码的方法,称为 Screenplay图案。

In the next chapter, we will look at a more advanced way of writing step definition code called the Screenplay Pattern.


1  在某些方法中(例如我们将在本书后面讨论的动作类和剧本模式),我们进一步拓展了这个概念,并且这成为了页面对象类的唯一责任。

1  In some approaches (such as the Action classes and Screenplay Patterns, which we will look at later in this book), we take this concept even further, and this becomes the only responsibility of the Page Object classes.

12 使用 Screenplay 模式实现可扩展的测试自动化

12 Scalable test automation with the Screenplay Pattern

本章封面

This chapter covers

  • 以 Actor 为中心的测试和 Screenplay 模式
  • Actor-centric testing and the Screenplay Pattern
  • 使用 Screenplay Interactions 与应用程序交互
  • Interacting with the application with Screenplay Interactions
  • 使用能力和问题查询系统状态
  • Querying the state of the system with abilities and questions
  • 将交互分组在一起以模拟更高级别的业务任务
  • Grouping interactions together to model higher-level business tasks
  • 将 Screenplay 代码与 Cucumber 集成
  • Integrating Screenplay code with Cucumber

随着测试自动化框架的发展,您很快意识到最大的问题不是知道如何自动化每个新场景,而是能够添加这些新场景,同时保持代码库简单且易于维护。随着测试套件与不同的 UI 组件、API、数据库等交互,保持代码简单一致会变得越来越困难。

As your test automation framework grows, you quickly realize that your biggest problem isn’t knowing how to automate each new scenario; it’s being able to add these new scenarios while at the same time keeping your code base simple and maintainable. As your test suite interacts with different UI components, APIs, databases and more, keeping your code simple and consistent can be harder and harder to do.

我们将在本章中介绍 Screenplay 模式,这是一种随着框架的发展而控制这种复杂性的方法。Screenplay 模式是一种以参与者为中心的可执行规范建模方法。它使用函数组合来帮助您捕获领域语言,并以技术和非技术受众都易于理解的方式表示系统需要支持的各种工作流。

The Screenplay Pattern, which we will be covering in this chapter, is an approach to keeping this complexity under control as our frameworks grow. The Screenplay Pattern is an actor-centered approach to modeling executable specifications. It uses functional composition to help you capture the domain language and represent the various workflows your system needs to support in a way that’s easy to understand by both technical and nontechnical audiences.

让我们从一个简单的例子开始,看看 Screenplay 模式如何帮助我们编写更简洁、更易于维护的测试自动化代码。Screenplay 帮助我们根据用户想要完成的任务以及用户执行这些任务需要与应用程序进行的交互来建模用户行为。在本章中,我们将了解如何对用户行为进行建模,使我们的代码更具可读性和可重用性,同时也使我们更容易构建易于维护和扩展的大型测试套件。

Let’s start with a quick example of how the Screenplay Pattern can help us write cleaner, more maintainable test automation code. Screenplay helps us model user behavior in terms of the tasks a user wants to achieve and the interactions with the application that the user needs to do to perform these tasks. In this chapter, we will see how modeling user behavior makes our code both more readable and more reusable, and also makes it easier to build large test suites that are easy to maintain and extend.

12.1 什么是剧本模式?为什么我们需要它?

12.1 What is the Screenplay Pattern, and why do we need it?

在第 8、9 和 10 章中,我们了解了其他一些有助于保持测试自动化代码整洁的方法。页面对象就是一个例子。我们已经了解了页面对象模型如何帮助将低级 WebDriver 代码与步骤定义代码分开,从而减少重复并简化步骤定义代码。

In chapters 8, 9, and 10, we saw a few other approaches to help keep your test automation code clean. Page Objects are one example. We’ve seen how the Page Object model can help to separate low-level WebDriver code from your step definition code, which can reduce duplication and simplify your step definition code.

但是,随着应用程序的增长,页面对象可能会导致两个主要问题。首先,当测试过于依赖页面对象时,它们往往会过分强调用户界面的构建方式。它们花费大量时间来描述哪些字段位于哪些页面上,以及如何从一个页面导航到另一个页面,这使得我们更难退后一步,了解用户真正想要实现的业务目标 — 更不用说,就其本质而言,页面对象只专注于通过图形用户界面建模交互。这意味着将 API、批处理或基于数据库的交互合并到测试中可能会感觉很别扭,并且与基于 UI 的交互不一致。

However, Page Objects can lead to two major problems as our application grows. First, when tests rely too heavily on Page Objects alone, they tend to place too much emphasis on how the user interface is built. They spend a lot of time describing which fields are on which pages, and how to navigate from one page to another, which makes it harder to step back and see what the user is really trying to achieve in business terms—not to mention that, by their very nature, Page Objects are focused on modeling interactions only with the graphical user interface. This means that incorporating API, batch processing, or database-based interactions into your tests might feel awkward and inconsistent with the UI-based interactions.

第二个问题是维护。对于较大的应用程序,Page Object 类通常会尝试做太多事情。例如,他们尝试对太多不相关的字段或页面元素进行建模,仅仅因为它们出现在应用程序的同一页面上。或者他们使用复杂的类层次结构来模仿实际应用程序中页面的组织方式。

A second problem is maintenance. For larger applications, Page Object classes often try to do way too much. For example, they try to model too many unrelated fields or page elements, simply because they appear on the same page of an application. Or they use complex class hierarchies to try to imitate the way pages are organized in the actual application.

避免这些问题的一种方法是在我们的场景描述的业务行为(内容)和我们的代码必须做什么才能实现此行为(方法)之间添加不同的抽象层。这有助于使页面对象更简单、更易于理解,并释放我们的测试代码,以便将更多精力放在用户旅程和业务任务上。

One way to avoid these problems is to add a different layer of abstraction between the business behavior our scenarios describe (the what) and what our code must do to deliver this behavior (the how). This helps to keep the Page Objects simpler and easier to understand and frees up our test code to focus more on the user journey and business tasks instead.

我们在第 11 章中介绍的 Action 类模式是实现这种分离的一种方法。使用 Action 类,行为被建模为方法或函数;每个方法描述一个特定的业务任务。让我们看一个实际的 Action 类模式的具体示例。在图 12.1 中,我们可以看到我们的常旅客应用程序的搜索页面。

The Action classes pattern that we looked at in chapter 11 is one way to make this separation. With Action classes, behavior is modeled as methods or functions; each method describes a particular business task. Let’s look at a concrete example of the Action classes pattern in practice. In figure 12.1 we can see the search page of our Frequent Flyer application.

图 12.1 我们的飞行常客应用程序的搜索屏幕接受我们需要输入的许多搜索条件。

Figure 12.1 The search screen of our Frequent Flyer application accepts a number of search criteria that we need to enter.

在第 11 章中,我们看到了测试此功能的一个场景示例:

In chapter 11 we saw an example of a scenario that tests this feature:

规则:旅客可以按出发地、目的地和旅行等级进行搜索
  场景概述:按旅行等级搜索航班
    假设Amy 以 Frequency Flyer 注册会员身份登录
    她搜索符合以下条件的航班
时      | 从 | 至 | 旅行舱位 |
      | <从> | <至> | <旅行等级> |
    那么回程航班应该符合旅行等级 <Travel Class>
  
  例如      | 从 | 至 | 旅行舱位 |
      | 悉尼 | 香港 | 经济 |
      | 伦敦 | 纽约 | 高级经济舱 |
      | 首尔 | 香港 | 商务 |
Rule: Travelers can search by departure, destination, and travel class
  Scenario Outline: Searching for flights by travel class
    Given Amy is logged on as registered Frequency Flyer member
    When she searches for flights with the following criteria
      | From   | To   | Travel Class   |
      | <From> | <To> | <Travel Class> |
    Then the returned flights should match the travel class <Travel Class>
  
  Examples:
      | From   | To        | Travel Class    |
      | Sydney | Hong Kong | Economy         |
      | London | New York  | Premium Economy |
      | Seoul  | Hong Kong | Business        |

我们还了解了如何使用 Action 类实现此场景。when 子句的代码使用了名为SearchFlights执行此操作。此类包含 Amy 执行搜索时需要执行的各个步骤的方法。代码如下所示:

We also saw how to implement this scenario using Action classes. The code for the when clause used an Action class called SearchFlights to perform this action. This class has methods for the various steps Amy needs to do when she performs a search. The code looked something like this:

@步骤
搜索航班 搜索航班;
 
@When(“她/他搜索符合以下条件的航班”)
公共无效执行搜索(FlightSearch搜索){
    searchFlights.from(search.from())
                 .to(搜索.to())
                 .inTravelClass(搜索.travelClass())
                 .and查看结果();
}
@Steps
SearchFlights searchFlights;
 
@When("she/he searches for flights with the following criteria")
public void performSearch(FlightSearch search) {
    searchFlights.from(search.from())
                 .to(search.to())
                 .inTravelClass(search.travelClass())
                 .andViewResults();
}

班级SearchFlights反过来,它又包含与应用程序本身交互的方法。例如,要选择出发机场,Serenity BDD 代码可能如下所示:

The SearchFlights class in turn contained methods that interacted with the application itself. For example, to select the departure airport, the Serenity BDD code might look something like this:

公共 void to(字符串目标){
    $("#destination").type(目的地);                   
    waitingForNoLongerThan(1).second()                     
        .find("/span[contains(.,'{0}')]", 目标)     
        .click();                                          
  
}
public void to(String destination) {
    $("#destination").type(destination);                  
    waitingForNoLongerThan(1).second()                    
        .find("/span[contains(.,'{0}')]", destination)    
        .click();                                         
  
}

在目的地字段中输入目的地

Enters the destination into the destination field

等待最多一秒钟,让下拉菜单填充内容

Waits for up to one second for the dropdown to populate

查找包含目的地城市的下拉条目

Finds a dropdown entry containing the destination city

然后点击匹配的目标条目

Then clicks on the matching destination entry

这种方法简洁易读,适用于简单场景。但它的可重用性不如我们所希望的那样:每当我们在应用程序的任何地方遇到类似的下拉字段时,都需要重复此处显示的下拉逻辑。此代码还隐含地假设只有一个外部参与者与系统交互,因此如果涉及多个参与者,事情就会变得棘手,工作流不同部分之间的界限就会变得模糊。

This approach is clean and readable and works well for simple scenarios. It is not as reusable as we might like, though: the dropdown logic shown here would need to be duplicated whenever we encounter a similar dropdown field anywhere in the application. This code also makes an implicit assumption that there’s only one external actor interacting with the system, so things can become tricky and boundaries between the different parts of a workflow blurred if more than one actor is involved.

在实际项目中,许多场景确实需要多个参与者。当我们测试复杂的业务应用程序时,我们经常需要演示多用户工作流,其中不同的活动由不同的人执行。例如,贷款申请可能需要客户提出申请,然后需要一个或多个银行员工审查并批准或拒绝该申请。聊天系统和游戏是各种参与者相互交互的其他常见示例。

And in real-world projects, many scenarios do need more than one actor. When we test complex business applications, we often need to demonstrate multi-user workflows, with different activities performed by different people. For example, a loan application may need a client to make the application, and then one or more bank employees to review and approve or reject the application. Chat systems and games are other common examples where various actors interact with each other.

Action 类方法的另一个限制是重用。虽然重用单个方法很容易,但如果我们想将一系列方法(例如,搜索航班、选择航班和预订航班)组合成单个任务(例如,进行预订),我们需要为此创建一个新方法。这会导致 Action 类很大,包含很多方法。

Another limitation of the Action-class approach is reuse. While it is easy to reuse individual methods, if we want to combine a sequence of methods (say, search flights, select a flight, and book the flight) into a single task (say, make a booking), we need to create a new method for that. This can lead to large Action classes with lots of methods.

Screenplay 模式提出了一种不同的方法,许多测试人员认为这种方法更灵活、更优雅。Screenplay 采用以参与者为中心的方法,我们在此建模与应用程序交互的参与者,以及这些参与者为实现其目标而执行的任务。一旦编写完成,任务就特别容易重用和组合,这使得该模式非常适合大型应用程序。

The Screenplay Pattern proposes a different approach, an approach that many testers find more flexible and more elegant. Screenplay uses an actor-centric approach, where we model actors that interact with the application, as well as the tasks that these actors perform to achieve their goals. Once written, tasks are particularly easy to reuse and to combine, which makes this pattern well suited to large applications.

使用剧本模式,我们可以使用以下代码自动执行场景的第二步,“当她搜索具有以下条件的航班时”:

Using the Screenplay Pattern, we could automate the second step of our scenario, “When she searches for flights with the following criteria,” using the following code:

@When("{actor} 搜索符合以下条件的航班")
public void performSearch(Actor 旅行者,FlightSearch 搜索标准){
    traveler.attemptsTo(                                       
            SearchFlights.matchingCriteria(searchCriteria)    。❷
    (英文):
}
@When("{actor} searches for flights with the following criteria")
public void performSearch(Actor traveler, FlightSearch searchCriteria) {
    traveler.attemptsTo(                                      
            SearchFlights.matchingCriteria(searchCriteria).   
    );
}

在剧本中,演员(在这里是旅行者)是大多数活动的起点。

In Screenplay, actors (in this case the traveler) are the starting point of most activities.

参与者执行任务,这些任务由命令对象序列表示,并作为参数传递给 attemptsTo() 方法。

Actors perform tasks, which are represented by sequences of command objects and passed as parameters to the attemptsTo() method.

班级SearchFlights封装此搜索表单的逻辑,并将交互细节隐藏在业务可读的外观后面:

The SearchFlights class encapsulates the logic of this search form and hides the interaction details behind a business-readable façade:

公共类搜索航班{
    公共静态可执行匹配(FlightSearch searchCriteria){
        return Task.where("搜索匹配的航班",                
                Navigate.toBookFlights(),                               
                类型.theValue(searchCriteria.from())                    
                    .进入(SearchFlightsForm.FROM),                      
                类型.theValue(searchCriteria.to())                      
                    .into(SearchFlightsForm.TO),
                选择.option(searchCriteria.travelClassName())         
                      来自(SearchFlightsForm.TRAVEL_CLASS_DROPDOWN),
                Click.on(SearchFlightsForm.SEARCH_BUTTON),              
                WaitUntil.the(SearchResultsList.SEARCH_RESULTS,isVisible())
                         .forNoMoreThan(5).seconds()                    
        (英文):
    }
}
public class SearchFlights {
    public static Performable matching(FlightSearch searchCriteria) {
        return Task.where("Search for matching flights",               
                Navigate.toBookFlights(),                              
                Type.theValue(searchCriteria.from())                   
                    .into(SearchFlightsForm.FROM),                      
                Type.theValue(searchCriteria.to())                     
                    .into(SearchFlightsForm.TO),
                Select.option(searchCriteria.travelClassName())        
                      .from(SearchFlightsForm.TRAVEL_CLASS_DROPDOWN),
                Click.on(SearchFlightsForm.SEARCH_BUTTON),             
                WaitUntil.the(SearchResultsList.SEARCH_RESULTS,isVisible())
                         .forNoMoreThan(5).seconds()                   
        );
    }
}

一份可读的任务描述,用于测试报告

A human-readable description of the task, to be used in test report

打开预订航班页面

Opens the book flights page

在输入栏中输入出发城市

Enters the Departure city in an input field

在输入字段中输入目的地城市

Enters the Destination city in an input field

在下拉菜单中选择旅行舱位

Selects a travel class in a dropdown menu

点击搜索按钮

Clicks on the search button

等待结果出现

Waits until the results appear

即使不理解该模式的细节,这段代码仍然非常易读。每个步骤都描述了参与者正在做什么以及他们正在与哪些元素进行交互。从很多方面来说,这就是剧本模式的精髓:在每一层,我们都专注于描述该层正在发生的事情,而将如何做留给下面的层。

Even without understanding the finer points of the pattern, this code remains very readable. Each step describes what the actor is doing and what element they are interacting with. In many ways that is the essence of the Screenplay Pattern: at each layer, we focus on describing what is happening at that layer, leaving the how for the layer underneath.

页面对象架构使用解决方案的语言来描述事物(页面、UI 元素等),而使用 Screenplay,我们使用业务或问题域的语言来建模事物。这样,Screenplay 模式可以非常自然地与行为驱动的验收测试编写方法配合使用。我们将在本文的其余部分探索 Screenplay 模式时更详细地了解此代码的工作原理章。

Whereas a Page Object architecture describes things in the language of the solution (pages, UI element, etc.), with Screenplay we model things using the language of the business or the problem domain. In this way, the Screenplay Pattern works very naturally with a behavior-driven approach to writing acceptance tests. We’ll see how this code works in more detail as we explore the Screenplay Pattern in the rest of the chapter.

剧本模式的起源

The origins of the Screenplay Pattern

过去几年来,Screenplay 模式越来越受欢迎。然而,这并不是一个新想法——其核心概念已经存在了一段时间。

The Screenplay Pattern has grown in popularity over the past few years. However, it is not a new idea—the core concepts have been around for a while.

这一切都始于敏捷联盟功能测试工具研讨会(AAFTT) 于2007年成立。

It all started at the Agile Alliance Functional Testing Tools workshop (AAFTT) back in 2007.

“赞美抽象”凯文·劳伦斯的演讲启发了 Antony Marcano 基于 Kevin 的想法实现流畅的 DSL,即使用交互设计师的语言来建模验收测试中的抽象层。在 Andy Palmer 的帮助下,这种流畅的 DSL 在一年后(2008 年)变成了 JNarrate。

“In Praise of Abstraction,” a talk given by Kevin Lawrence, inspired Antony Marcano to implement a fluent DSL based on Kevin's idea to use the language of interaction designers to model the layers of abstraction in an acceptance test. With the help of Andy Palmer, this fluent DSL became JNarrate a year later (2008).

2012 年底,Antony 和 Andy 与 Jan Molak 联手。他们对 Kevin 的模型进行了实验,并希望解决页面对象模式的缺陷问题,并将 SOLID 设计原则应用于验收测试,这就是 2013 年被称为screenplay-jvmhttps://github.com/screenplay/screenplay-jvm)。

In the late 2012, Antony and Andy joined forces with Jan Molak. Their experiments with Kevin's model, combined with a desire to address problems with shortcomings of the Page Object pattern and apply SOLID design principles to acceptance testing is what became known in 2013 as screenplay-jvm (https://github.com/screenplay/screenplay-jvm).

2015 年,当 Antony、Andy 和 Jan 开始与 John Ferguson Smart 合作时,后来被称为 Screenplay 模式的技术进入了 Serenity BDD,并从那里进入了更广泛的测试自动化社区。

And in 2015, when Antony, Andy, and Jan started working with John Ferguson Smart, what became known as the Screenplay Pattern found its way into Serenity BDD, and from there into the broader test automation community.

12.2 剧本基础

12.2 Screenplay fundamentals

尽管尽管 Screenplay 模式的名称中没有“屏幕”一词,但它与计算机屏幕没有任何关系。相反,它是一种以用户为中心的验收测试建模方法,可以与我们系统的任何外部接口进行交互。

Despite the word “screen” in its name, the Screenplay Pattern has nothing to do with computer screens. On the contrary, it is a general method of modeling user-centered acceptance tests interacting with any external interface of our system.

该模式的名称灵感来自电影制作领域。在电影制作领域,剧本描述了参与表演的演员、他们的对话以及他们执行的各种任务。在测试自动化领域,我们使用演员来表示系统的用户。我们将他们的行为和微目标建模为任务,这些任务指示演员执行哪些活动以及检查哪些内容。这些活动序列使用您创建的领域特定测试语言来表达,并由您定义的各个演员执行,成为剧本。

The name of the pattern takes inspiration from the world of film production. There, a screenplay describes the actors who take part in the performance, their dialogues, and the various tasks they perform. Here, in the world of test automation, we use actors to represent the users of our system. We model their behavior and micro-goals as tasks, which instruct the actors what activities to perform and what things to check. Those sequences of activities, expressed using the domain-specific test language you create and performed by the various actors you define, become screenplays.

这种方法有助于我们更清楚地描述用户和系统行为,并让我们能够更轻松地以可重用代码的形式封装行为。它还引导我们走向干净、设计良好的抽象层,这有助于让我们的代码库更易于理解和维护。这种对用户和用户行为的关注使 Screenplay 模式非常适合许多 BDD 场景。

This approach helps us to describe both user and system behavior more clearly and makes it easier to encapsulate behavior in the form of reusable code. It also steers us toward clean, well-designed layers of abstraction, which helps to make our code base easier to understand and maintain. This focus on users and user behavior makes the Screenplay Pattern a great fit for many BDD scenarios.

Screenplay 模式的美妙之处在于它的简洁。它由五个元素组成,我们可以将它们组合起来表达我们所需的任何功能验收测试,无论它有多复杂或多简单:

The Screenplay Pattern is beautiful in its simplicity. It’s made up of five elements we combine to express any functional acceptance test we need, no matter how sophisticated or how simple it has to be:

  • 参与者——代表与我们交互的用户和外部系统

  • Actors—Who represent users and external systems interacting with ours

  • 交互——代表参与者可以在我们的系统中执行的最基本活动,例如单击按钮或向 API 发送请求

  • Interactions—Which represent the most basic activities an actor can perform in our system, such as clicking on a button or sending a request to an API

  • 能力——使参与者能够通过系统的各种界面与系统进行交互

  • Abilities—Which enable the actors to interact with the system through its various interfaces

  • 问题——当参与者回答这些问题时,会提供有关系统状态的信息

  • Questions—Which, when answered by the actors, provide information about the state of the system

  • 任务——使我们能够对交互或其他任务的序列进行分组、组合和重用

  • Tasks—Which allow us to group, combine, and reuse sequences of interactions or other tasks

图 12.2 显示了这些元素如何组合在一起。在以下部分中,我们将更详细地介绍这些组件。细节。

You can see an overview of how these elements fit together in figure 12.2. In the following sections we’ll look at each of these components in more detail.

图 12.2 剧本模式是一个以演员为中心的模型。

Figure 12.2 The Screenplay Pattern is an actor-centric model.

12.3 什么是演员?

12.3 What is an actor?

Screenplay 模式是一种以用户为中心的模型,这意味着参与者是其关键元素,也是我们编写的每个测试场景的起点。参与者代表与被测系统交互的某人或某物,例如用户,甚至是外部系统。参与者还可以对应于我们在第 7 章中介绍的角色(参见第 7.6.5 节),以及我们一直在 Gherkin 场景中使用的角色。

The Screenplay Pattern is a user-centric model, which means that actors are its key element and a starting point of every test scenario we write. An actor represents someone or something who interacts with the system under test, such as a user, or even an external system. Actors can also correspond to the personas we introduced in chapter 7 (see section 7.6.5) and that we have been using in our Gherkin scenarios.

当参与者代表用户角色时,我们通常会给他们起一些容易记住的名字,例如旅行者 Tracy 或客户经理 Amy。以下是我们如何使用 Serenity BDD 在 Java 中定义新的 Screenplay 参与者的示例:

When actors represent user personas, we often give them easy-to-remember names, such as Tracy the Traveler or Amy the Account Manager. Here’s an example of how we define a new Screenplay actor in Java using Serenity BDD:

    演员 tracy = Actor.named("Tracy");
    Actor tracy = Actor.named("Tracy");

但定义演员只是开始。毕竟,一个从不出现在舞台上的明星算不上明星。演员只有在电影中扮演角色时才会变得有趣。同样,在剧本场景中,我们希望看到演员们行动起来,执行业务任务并完成任务。让我们看看这是如何做到的發生。

But defining an actor is just the start. After all, a star who never appears on stage isn’t much of a star. An actor only becomes interesting when they play a role in the movie. Likewise, in a Screenplay scenario, we want to see our actors in action, performing business tasks and getting stuff done. Let’s look at how this happens.

Screenplay:一种设计模式,而不是一个库

Screenplay: A design pattern, not a library

在本章的其余部分,我们将使用 Screenplay 模式的 Serenity BDD 实现给出 Java 代码示例。但是,请务必记住,Screenplay 是一种设计模式,而不是库,并且有许多 Screenplay 实现。其中一些比较著名的实现包括 Serenity/JS (JavaScript/TypeScript)、ScreenPy (Python)、Boa Constrictor (.NET) 和 Cucumber Screenplay (JavaScript/TypeScript)。

In the rest of this chapter, we’ll be giving code examples in Java using the Serenity BDD implementation of the Screenplay Pattern. However, it’s important to remember that Screenplay is a design pattern, not a library, and there are many Screenplay implementations out there. Some of the more notable ones include Serenity/JS (JavaScript/TypeScript), ScreenPy (Python), Boa Constrictor (.NET), and Cucumber Screenplay (JavaScript/TypeScript).

12.4 参与者执行任务

12.4 Actors perform tasks

喜欢电影中的真实演员和剧本演员在测试中扮演着不同的角色。他们有明确的业务目标,例如购买机票、开具发票或批准贷款。为了实现这些目标,他们需要执行一系列业务任务,例如搜索航班、查找客户或进行信用检查。

Like real actors in a film, Screenplay actors have a role to play in a test. They have well-defined business goals they want to achieve, such as purchase a plane ticket, issue an invoice, or approve a loan. And to achieve these goals, they need to perform a sequence of business tasks, such as search for a flight, locate a customer, or perform a credit check.

这些任务以业务或领域语言表达,并以业务术语(而不是屏幕、按钮或 API)来建模参与者需要做的事情。例如,如果参与者(例如 Tracy)想要预订航班,她需要做的事情之一就是搜索可用的航班。

These tasks are expressed in business or domain language and model what the actor needs to do in business terms, not in terms of screens, buttons, or APIs. For example, if an actor (say, Tracy) wants to book a flight, one of the things she will need to do is search for the available flights.

这些任务可能由子任务组成;例如,要开具发票,演员需要查找客户详细信息并提供销售详细信息。要查找客户详细信息,他们需要使用 CRM 系统、搜索客户等等。在 Screenplay 测试中,我们将用一个任务来模拟此操作,可能如下所示:

Those tasks might be composed of subtasks; for example to raise an invoice an actor would need to find the customer details and provide the details of the sale. To find the customer details they’d need to use the CRM system, search for the customer, and so on. In a Screenplay test, we would model this action with a task, possibly like this:

tracy.尝试(
    BookAFlight.from("伦敦")
               .to("纽约")
               .inClass(商务) 
               .出发时间(2).天数()
(英文):
tracy.attemptsTo(
    BookAFlight.from("London")
               .to("New York")
               .inClass(Business) 
               .departingIn(2).days()
);

请注意,我们尝试使用业务语言来表达——任务没有指定任何特定的实现,可以用多种方式实现。但这里的关键是我们用业务术语来表示参与者需要做什么。

Notice how we try to talk the language of the business—the task does not specify any particular implementation and could be implemented in many ways. But here the key is that we are representing what the actor needs to do in business terms.

在本章后面,我们将回到任务,并了解如何创建自己的任务。但首先,我们需要了解 Screenplay 如何帮助我们模拟参与者与应用程序的交互,以执行这些任务任務。

We will come back to tasks and see how you can create your own, later in the chapter. But first, we need to understand how Screenplay helps us model how the actor interacts with the application in order to perform these tasks.

12.5 交互模型描述了参与者如何与系统交互

12.5 Interactions model how actors interact with the system

任务表示参与者如何看待世界,与任何特定实现无关。但要执行任务,参与者必须与被测系统进行交互,以便实现这些目标(见图 12.3)。

Tasks represent how the actor sees the world, independently of any particular implementation. But to perform a task, the actor must in turn interact with the system under test so that they can achieve these goals (see figure 12.3).

图 12.3 参与者使用交互类与被测系统进行交互。

Figure 12.3 Actors interact with the system under test using Interaction classes.

这些交互可以有多种形式。我们的参与者可能需要导航到特定的 URL、单击按钮或在字段中输入值,或者向 REST API 发送 HTTP 请求。

These interactions can come in many forms. Our actor may need to navigate to a particular URL, to click on a button or enter a value into a field, or maybe send an HTTP request to a REST API.

但无论我们与什么系统交互,在 Screenplay 模式中我们都使用相同的模式:actor.attemptsTo()方法,然后提供我们希望参与者执行的活动列表。(我们说“试图”而不是更自信的表达,例如“执行”或“做”,因为毕竟这是一个测试,可能会出错。)

But no matter what system we are interacting with, in the Screenplay Pattern we use the same pattern: the actor.attemptsTo() method, and then provide a list of the activities we would like our actor to perform. (We say “attempts to” rather than a more confident expression such as “performs” or “does” because, after all, this is a test, and something might go wrong.)

假设我们想让 Tracy 点击图 12.1 中的搜索图标,该图标可以通过 CSS 表达式“#search-button”来识别。点击此按钮的 Screenplay 代码如下所示:

Imagine that we want Tracy to click on the search icon in figure 12.1, which can be identified by the CSS expression “#search-button”. The Screenplay code to click on this button would look something like this:

tracy.attemptsTo(点击(“#search-button”));
tracy.attemptsTo(Click.on("#search-button"));

或者如果我们喜欢更明确的风格,我们可以使用这样的 Selenium 定位器:

Or if we prefer a more explicit style, we could use a Selenium locator like this:

tracy.attemptsTo(Click.on(By.id("搜索按钮")));
tracy.attemptsTo(Click.on(By.id("search-button")));

在这两种情况下,我们都使用Click交互类来完成实际工作。在底层,该类将找到该参与者的 WebDriver 实例并为我们执行适当的 Selenium 命令。Click是 Screenplay 库(如 Serenity BDD)为您提供的数百个内置交互类之一,因此您通常不必自己实现低级交互。

In both cases, we are using the Click interaction class to do the actual work. Under the hood this class will find the WebDriver instance for this actor and execute the appropriate Selenium commands for us. Click is one of hundreds of built-in interaction classes that Screenplay libraries like Serenity BDD provide for you out of the box, so you typically don’t have to implement the low-level interactions yourself.

12.5.1 参与者可以进行多重互动

12.5.1 Actors can perform multiple interactions

通常,参与者会想要做不止一件事来实现他们的目标。例如,当 Tracy 登录应用程序时,她需要输入她的电子邮件地址和密码,然后单击“登录”按钮(见图 12.4)。

Typically, an actor will want to do more than one thing to achieve their goals. For example, when Tracy logs on to the application, she needs to enter her email address and her password, and then click the Login button (see figure 12.4).

图 12.4 常旅客登录页面

Figure 12.4 The Frequent Flyers login page

为了实现这些操作,我们只需重复调用attemptsTo()方法像这样(Enter交互类我们在这里使用的方法相当于 SeleniumsendKeys()方法;它将文本值输入到指定的字段中):

To implement these actions we could simply repeat the calls to the attemptsTo() method, like this (the Enter interaction class that we use here is the equivalent to the Selenium sendKeys() method; it enters a text value into a specified field):

tracy.attemptsTo(Open.url(“http://localhost:3000/”));
tracy.attemptsTo(Click.on("//按钮[normalize-space()='登录']");
tracy.attemptsTo(输入.theValue(“tracy@traveler.com”)。输入(“#email”));
tracy.attemptsTo(输入.theValue("secretPassword")。进入("#password"));
tracy.attemptsTo(点击("#login-button"));
tracy.attemptsTo(Open.url("http://localhost:3000/"));
tracy.attemptsTo(Click.on("//button[normalize-space()='Login']");
tracy.attemptsTo(Enter.theValue("tracy@traveler.com").into("#email"));
tracy.attemptsTo(Enter.theValue("secretPassword").into("#password"));
tracy.attemptsTo(Click.on("#login-button"));

但这会变得重复,分散操作的注意力,使代码更难阅读。更简洁的方法是使用对attemptsTo()方法的单一调用并传递一系列活动:

But this would become repetitive and distract from the flow of the actions, making the code harder to read. A cleaner approach would be to use a single call to the attemptsTo() method and pass in a sequence of activities:

tracy.尝试(
    Open.url("http://localhost:3000/"),                      
    Click.on("//button[normalize-space()='登录']"),         
    输入.theValue(“tracy@traveler.com”)。输入(“#email”),     
    Enter.theValue("secretPassword").into("#password"),      
    Click.on("#login-button")                                
(英文):
tracy.attemptsTo(
    Open.url("http://localhost:3000/"),                     
    Click.on("//button[normalize-space()='Login']"),        
    Enter.theValue("tracy@traveler.com").into("#email"),    
    Enter.theValue("secretPassword").into("#password"),     
    Click.on("#login-button")                               
);

打开应用程序主页

Opens the application home page

进入登录页面

Goes to the login page

输入邮箱

Enters the email

输入密码

Enters the password

点击登录按钮

Clicks on the login button

此类交互类旨在使与用户界面的交互变得简单直观。Serenity BDD(http://serenity-bdd.infohttps://serenity-bdd.github.io)和 Serenity/JS(https://serenity-js.org)等 Screenplay 实现通常捆绑了大量用于不同情况的交互。您可以在以下位置看到一些更常见的 UI Screenplay 交互类表 12.1。

Interaction classes like these are designed to make interacting with a user interface simple and intuitive. Screenplay implementations such as Serenity BDD (http://serenity-bdd.info and https://serenity-bdd.github.io) and Serenity/JS (https://serenity-js.org) typically come bundled with a large number of interactions for different situations. You can see some of the more common UI Screenplay interaction classes in table 12.1.

表 12.1 一些常见的 Screenplay UI 交互类

Table 12.1 Some common Screenplay UI Interaction classes

相互作用

Interaction

用法

Usage

例子

Example

Clear

Clear

清除文本字段

Clear a text field

Clear.field("#email")

Clear.field("#email")

Click

Click

点击一个元素

Click on an element

Click.on("#login-button")

Click.on("#login-button")

Enter

Enter

在字段中输入值

Type a value into a field

Enter.theValue("secretPassword")

Enter.theValue("secretPassword")

        .into("#password")

        .into("#password")

Open

Open

打开给定的 URL(在 Serenity BDD 中)

Open a given URL (in Serenity BDD)

Open.url("https://www.google.com")

Open.url("https://www.google.com")

Navigate

Navigate

打开给定的 URL(在 Serenity/JS 中)

Open a given URL (in Serenity/JS)

Navigate .to("https://www.google.com")

Navigate .to("https://www.google.com")

Scroll

Scroll

将浏览器滚动到指定元素

Scrolls the browser to the specified element

Scroll.to("#login-button")

Scroll.to("#login-button")

SelectFromOptions

SelectFromOptions

选择下拉列表值(在 Serenity BDD 中)

Select a dropdown list value (in Serenity BDD)

SelectFromOptions

SelectFromOptions

        .byVisibleText("Economy")

        .byVisibleText("Economy")

        .from("#travel-class")

        .from("#travel-class")

Select

Select

选择下拉列表值(在 Serenity JS 中)

Select a dropdown list value (in Serenity JS)

Select.option("Economy")

Select.option("Economy")

        .from(travelClass)

        .from(travelClass)

12.5.2 交互是对象,不是方法

12.5.2 Interactions are objects, not methods

一个重要的细节,乍一看很容易被忽略,就是代码中显示的每个交互都不是类的方法,而是一个独立的对象。这是 Screenplay 的核心原则,也是该模式具有很大灵活性的原因。

One important detail, that is easy to overlook at first glance, is that each interaction in the code shown is not a method of a class, but an object in its own right. This is a core Screenplay principle and is what gives the pattern a lot of its flexibility.

在传统的 WebDriver 代码中,如果您想单击一个按钮,您可以按照以下方式编写代码:

In conventional WebDriver code, if you want to click on a button, you might write code along the following lines:

WebElement loginButton = driver.findElement(By.id("#login-button"));   
登录按钮.点击();                                                   
WebElement loginButton = driver.findElement(By.id("#login-button"));  
loginButton.click();                                                  

找到我们想要交互的元素

Locates the element we want to interact with

调用此对象的 click() 方法

Invokes the click() method on this object

但在剧本中,我们采用了不同的方法。正如我们所见,演员使用的attemptsTo()方法执行一系列交互,1因此等效代码如下所示:

But in Screenplay, we take a different approach. As we have seen, the actor uses the attemptsTo() method to perform a series of interactions,1 so the equivalent code would look something like this:

tracy.attemptsTo(                
    Click.on("#login-button")    
(英文):
tracy.attemptsTo(               
    Click.on("#login-button")   
);

表演动作的演员

The actor performing the action

正在执行的操作

The action being performed

从表面上看,第二个代码示例与第一个非常相似。在这两种情况下,我们都点击了一个按钮,但不是调用click()方法,我们创建并传递该类的一个新Click实例方法attemptsTo()

On the surface, the second code sample looks very similar to the first. In both cases, we are clicking on a button, but rather than invoking the click() method, we create and pass a new instance of the Click class to the attemptsTo() method.

这为我们提供了更加灵活的架构。当我们想要添加新行为时,我们不必修改现有的类;我们只需创建一个新的交互类。而且,由于不必对现有代码进行太多更改,我们降低了无意中破坏某些东西的风险。

This gives us a much more flexible architecture. We don’t have to modify our existing classes when we want to add new behavior; we simply create a new interaction class. And by not having to change our existing code so much, we reduce the risk of inadvertently breaking something.

例如,WebDriver没有执行双击的方法。如果我们想这样做,我们需要引入 WebDriverActions并使用不同的编码风格:

For example, the WebDriver class has no method to perform a double-click. If we want to do that, we need to introduce the WebDriver Actions class and use a different coding style:

WebElement loginButton = driver.findElement(By.id("#login-button"));
动作动作=新动作(驱动程序);
动作.双击(登录按钮)。执行();
WebElement loginButton = driver.findElement(By.id("#login-button"));
Actions action = new Actions(driver);
action.doubleClick(loginButton).perform();

另一方面,在 Screenplay 中,我们只是使用不同的交互类:

In Screenplay, on the other hand, we simply use a different interaction class:

tracy.尝试(
    DoubleClick.on("#login-button")
(英文):
tracy.attemptsTo(
    DoubleClick.on("#login-button")
); 

Screenplay 库通常附带各种适用于不同情况的捆绑交互类,但您也可以自己轻松编写。我们将在本章后面介绍如何编写章。

Screenplay libraries generally come with a wide range of bundled interaction classes for different situations, but they are easy to write yourself, too. We’ll see how later in this chapter.

剧本参与者和交互使用命令模式

Screenplay Actors and Interactions use the Command pattern

在面向对象设计中,命令模式一种设计模式,其中包含执行某些操作所需的所有信息的对象被传递给可以执行该操作的函数或方法。这有点像你在餐馆里:服务员接受你的订单,把它写在一张纸上(对象),然后把这张纸传到厨房,在那里为你准备饭菜。同样,交互对象只包含参与者应该做什么的指令;是参与者执行这些指令。

In Object-Oriented Design, the Command Patterna is a design pattern where an object containing all the information necessary to perform some action is passed to a function or method that can perform the action. It’s a bit like when you are in a restaurant: the waiter takes your order, writes it down on a piece of paper (the object), and passes this piece of paper to the kitchen, where your meal is prepared. In the same way the Interaction Object only contains instructions of what the actor should do; it is the actor who performs them.


一个  Erich Gamma 等人,《设计模式:可重用面向对象软件的元素》(Addison-Wesley,1994 年)。

a  Erich Gamma et al., Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).

12.5.3 交互可以执行等待以及操作

12.5.3 Interactions can perform waits as well as actions

什么时候使用现代应用程序时,我们经常需要等待事情发生。也许我们需要等待 API 调用将结果返回到屏幕,等待后端系统更新记录的状态,或者等待我们的应用程序收到一些消息或事件。

When working with modern applications, we often need to wait for things to happen. Maybe we need to wait for an API call to return results to the screen, for a backend system to update the state of a record, or for our application to receive some message or event.

Screenplay 模式为我们提供了一种优雅的方式来等待特定状态。例如,当我们搜索航班时,我们需要等待服务器返回结果。这在视觉上由一个旋转器表示(见图 12.5)。

The Screenplay Pattern gives us an elegant way to wait for a particular state. For example, when we search for flights, we need to wait until the results are returned from the server. This is visually represented by a spinner (see figure 12.5).

图12.5 等待搜索结果

Figure 12.5 Waiting for the search results

WaitUntil在 Serenity BDD 中,我们可以使用交互等待此旋转器消失, 像这样:

In Serenity BDD, we could wait for this spinner to disappear using the WaitUntil interaction, like this:

tracy.尝试(
       WaitUntil.the(".block-ui-spinner", isNotVisible())     
                .forNoMoreThan(10).seconds()                  
tracy.attemptsTo(
       WaitUntil.the(".block-ui-spinner", isNotVisible())    
                .forNoMoreThan(10).seconds()                 
   )

等待旋转器消失

Waits until the spinner disappears

将最大等待时间设置为 10 秒

Sets a maximum time to wait to 10 seconds

或者,我们可以等到搜索结果在新页面上可见,如下所示:

Alternatively, we could wait until the search results are visible on the new page, like this:

tracy.尝试( 
       WaitUntil.the(".flight-container", isVisible())      
                .forNoMoreThan(10).seconds()                
tracy.attemptsTo( 
       WaitUntil.the(".flight-container", isVisible())     
                .forNoMoreThan(10).seconds()               
   )

等待搜索结果出现

Waits until the search results appear

将最大等待时间设置为 10 秒

Sets a maximum time to wait to 10 seconds

这些表达isNotVisible()isVisible()来自WebElementStateMatchers课堂,其中包含许多类似的方法。表 12.2 列出了一些较常见的方法。

The expressions isNotVisible() and isVisible() come from the WebElementStateMatchers class, which contains many similar methods. Some of the more common ones are listed in table 12.2.

我们可以使用 SeleniumExpectedConditions访问各种等待条件:

We can use the Selenium ExpectedConditions class to access a wide range of wait conditions:

tracy.尝试( 
        WaitUntil.the(titleIs("搜索结果"))             
                 .forNoMoreThan(Duration.of(3,SECONDS))     
    (英文):
tracy.attemptsTo( 
        WaitUntil.the(titleIs("Search Results"))            
                 .forNoMoreThan(Duration.of(3, SECONDS))    
    );

我们使用ExpectedConditions.titleIs()方法来定义预期条件。

We use the ExpectedConditions.titleIs() method to define the expected condition.

和以前一样,我们也可以指定要等待多长时间。

As before, we can also specify how long we want to wait.

我们甚至可以等到某个任意函数完成,使用Wait

We could even wait until some arbitrary function is completed, using the Wait class:

tracy.尝试(
        Wait.until(() -> fileIsProcessed())。            
            .forNoMoreThan(Duration.of(3,SECONDS))     
    (英文):
tracy.attemptsTo(
        Wait.until(() -> fileIsProcessed()).           
            .forNoMoreThan(Duration.of(3, SECONDS))    
    );

我们等待布尔函数返回 true 值。

We wait for a Boolean function to return a value of true.

我们可以再次指定要等待多长时间。

Once again we can specify how long we want to wait.

如你所见,Screenplay 在等待条件方面给了我们很大的灵活性,但每个变体都遵循我们所见过的相同的一般风格和模式。远的。

As you can see, Screenplay gives us a great deal of flexibility when it comes to wait conditions, but each variation follows the same general style and patterns we have seen so far.

表 12.2 等待条件及用法

Table 12.2 Wait conditions and usages

等待条件

Wait Conditions

用法

Usage

例子

Example

isVisible()

isVisible()

等到元素渲染完成

Wait until the element is rendered

WaitUntil.the(".flight-container",

WaitUntil.the(".flight-container",

              isVisible())

              isVisible())

isNotVisible()a

isNotVisible()a

等到元素未渲染

Wait until the element is not rendered

WaitUntil.the("#login-button",

WaitUntil.the("#login-button",

              isNotVisible())

              isNotVisible())

isEnabled

isEnabled

元素已启用

Element is enabled

WaitUntil.the("#search-button",

WaitUntil.the("#search-button",

              isEnabled())

              isEnabled())

isPresent

isPresent

元素存在于 DOM 中

Element is present in the DOM

WaitUntil.the("#search-button",

WaitUntil.the("#search-button",

              isPresent())

              isPresent())

containsText

containsText

给定元素包含指定的文本值

A given element contains the specified text value

WaitUntil.the(".status-message",

WaitUntil.the(".status-message",

              containsText("Done"))

              containsText("Done"))

一个  大多数等待条件都有一个版本(例如isNotVisible()isNotEnabled()等等)。

a  Most wait conditions have a not version (e.g. isNotVisible(), isNotEnabled(), etc.).

12.5.4 Interactions 也可以与 REST API 进行交互

12.5.4 Interactions can also interact with REST APIs

因此到目前为止,我们一直专注于通过 UI 与应用程序交互。但 Screenplay 模式的一大优点是它的灵活性。参与者可以通过 Selenium WebDriver、Webdriver.io、Playwright 或其他技术与浏览器交互,具体取决于他们选择使用的交互类。但他们也可以通过其他方式与系统交互。

Thus far we’ve focused on interacting with the application through the UI. But one of the great things about the Screenplay Pattern is its flexibility. Actors can interact with the browser via Selenium WebDriver, Webdriver.io, Playwright, or some other technology, depending on the interaction classes they choose to use. But they can also interact with the system in other ways.

假设我们需要注册一个新用户,我们有两个选择。第一个选择是打开飞行常客网站,转到注册页面,输入详细信息并提交表单。第二个选择是使用 REST API。

Suppose we need to register a new user, and we have two options. The first option is to open the Frequent Flyer site, go to the registration page, enter the details, and submit the form. The second option is to use a REST API.

旅行者 trevor = new Traveler("trevor@traveler.com","secret");     
演员.尝试(
        Post.to(“/users”)                                           
            .with(request -> request.body(trevor))                  
(英文):
Traveler trevor = new Traveler("trevor@traveler.com","secret");    
actor.attemptsTo(
        Post.to("/users")                                          
            .with(request -> request.body(trevor))                 
);

创建新的旅行者进行注册

Creates a new traveler to register

发送 POST 请求...

Sends a POST request ...

...带有请求主体。

... with a request body.

我们将在下文中更详细地介绍如何使用 REST API 进行交互(无论是否使用 Screenplay)。下一个章。

We’ll look at interacting with REST APIs in more detail, both with and without Screenplay, in the next chapter.

12.6 能力是参与者与系统交互的方式

12.6 Abilities are how actors interact with the system

因此到目前为止,我们已经了解了参与者如何与被测系统交互以实现其目标。为此,他们可能需要使用 Web 浏览器、调用 API、查询数据库或与大型机交互 - 可能性无穷无尽!但这是如何工作的?这就是能力概念发挥作用的地方(见图 12.6)。

Thus far we’ve seen how actors can interact with the system under test to achieve their goals. To do that, they might need to use a web browser, make a call to an API, query a database, or interact with a mainframe—the possibilities are endless! But how does this work? This is where the concept of abilities comes into play (see figure 12.6).

图 12.6 我们赋予演员使他们能够与系统交互的能力。

Figure 12.6 We give actors abilities that enable them to interact with the system.

能力使演员能够与所有这些不同的系统进行交互。如果你愿意的话,它们赋予演员特殊能力。

Abilities are what enable an actor to interact with all these different systems. They give our actors their special powers, if you will.

例如,每个参与者都可以拥有自己的浏览器实例,因此我们需要告诉参与者他们应该使用哪个浏览器。在 Serenity BDD 和 Serenity/JS 中,我们可以通过使用以下BrowseTheWeb功能让参与者能够使用 WebDriver 与 Web 浏览器进行交互

For example, each actor can have their own browser instance, so we need to tell our actors which browser they should use. In Serenity BDD and Serenity/JS, we can give an actor the ability to interact with a web browser using WebDriver by using the BrowseTheWeb ability:

WebDriver driver = new ChromeDriver();      
演员 sally = Actor.named("Sally");         
sally.can(BrowseTheWeb.with(驱动程序));       
WebDriver driver = new ChromeDriver();     
Actor sally = Actor.named("Sally");        
sally.can(BrowseTheWeb.with(driver));      

创建一个新的WebDriver实例来打开Chrome浏览器。

Create a new WebDriver instance to open a Chrome browser.

创建新的演员。

Create a new actor.

Sally 现在可以使用我们在上一节中看到的所有 WebDriver 交互。

Sally can now use all the WebDriver interactions we saw in the previous section.

以类似的方式,我们可以赋予演员查询 REST 端点的能力:

In a similar way, we can give an actor the ability to query a REST endpoint:

    sally.can(CallAnApi.at("http:/ /my.api.server:8000"));
    sally.can(CallAnApi.at("http:/ /my.api.server:8000"));

一旦我们为演员赋予了能力,他们就可以使用这种能力进行互动或提问。例如BrowseTheWeb分配一个唯一WebDriver实例每个参与者。我们可以使用BrowseTheWeb.as()静态方法访问此实例假设我们想直接从参与者的WebDriver实例获取当前 URL。我们可以使用以下代码来实现这一点:

Once we have given an ability to our actors, they can use this ability to perform interactions or ask questions. For example, the BrowseTheWeb ability assigns a unique WebDriver instance to each actor. We can access this instance using the BrowseTheWeb.as() static method. Suppose we wanted to get the current URL directly from the actor’s WebDriver instance. We can do just that with the following code:

  字符串 currentUrl = BrowseTheWeb.as(actor).getDriver().getCurrentUrl();
  String currentUrl = BrowseTheWeb.as(actor).getDriver().getCurrentUrl();

这使得 Screenplay 成为一个非常可扩展的模型:您可以轻松添加新功能,而无需更改需要交互的每个新系统的参与者实现和。

This makes Screenplay a very extensible model: you can easily add new abilities without having to change the actor implementation for each new system you need to interact with.

能力是适配器模式的实现

Abilities are an implementation of the Adaptor Pattern

如果你熟悉面向对象设计模式,那么能力就是适配器模式的实现.a适配器是一种薄接口,允许两个系统协同工作,而无需更改任一系统的代码。因此,能力是围绕较低级别、特定于接口的客户端(例如 Web 浏览器驱动程序、HTTP 客户端、数据库客户端等)的薄包装器。当您需要使用外部系统完成任务时,您可以调用此接口与该系统交互。

If you are familiar with Object-Oriented Design patterns, an ability is an implementation of the Adaptor Pattern.a An adaptor is a thin interface that allows two systems to work together, without having to change the code in either system. An ability, then, is a thin wrapper around a lower-level, interface-specific client such as a web browser driver, a HTTP client, a database client, and so on. When you need to complete a task using an external system, you invoke this interface to interact with that system.


一个  Erich Gamma 等人

a  Erich Gamma et al.

12.7 编写我们自己的交互类

12.7 Writing our own interaction classes

参与者拥有能力的原因是为了在交互和问题类中使用它们来与被测系统交互并从中检索信息。大多数 Screenplay 实现通常都会附带大多数标准场景所需的所有交互类。

The reason an actor has abilities is to use them in interaction and question classes to interact with and retrieve information from the system under test. Most Screenplay implementations will generally come with all the interaction classes you will need for most standard scenarios.

交互类很容易编写。使用 Serenity BDD,最简单的方法是使用Interaction.where()方法,这使我们能够创建一个新Interaction动态地使用 Java 8 lambda 表达式。

Interaction classes are easy to write. With Serenity BDD, the simplest way to do it is to use the Interaction.where() method, which allows us to create a new Interaction class dynamically using a Java 8 lambda expression.

让我们看一个例子。假设我们需要切换到另一个窗口。使用 WebDriver,我们可以使用 WebDriverswitchTo()方法来实现这一点。如果我们想编写一个 Screenplay 交互类来执行此操作,我们可以编写类似这:

Let’s look at an example. Imagine we need to switch to another window. With WebDriver we can do that using the WebDriver switchTo() method. If we wanted to write a Screenplay interaction class to do this, we could write something like this:

公共类 SwitchTo {
    公共静态交互父级(){
        return Interaction.where("{0} 切换到父框架",
            theActor -> {                                                    
                WebDriver driver = BrowseTheWeb.as(theActor).getDriver();    
                驱动程序.switchTo().parentFrame();                             
            }
        (英文):
    }
}
public class SwitchTo {
    public static Interaction parent() {
        return Interaction.where("{0} switches to the parent frame",
            theActor -> {                                                   
                WebDriver driver = BrowseTheWeb.as(theActor).getDriver();   
                driver.switchTo().parentFrame();                            
            }
        );
    }
}

将 Actor 传递到闭包中,以执行低级 UI 操作

Passes the actor into a closure to perform low-level UI actions

使用 BrowseTheWeb 类来检索此参与者的浏览器

Uses the BrowseTheWeb class to retrieve this actor’s browser

使用参与者的浏览器切换到父框架

Switches to the parent frame using the actor’s browser

现在Sally可以使用这个交互类切换到父框架,如下所示:

Now Sally can use this interaction class to switch to the parent frame like this:

sally.尝试切换(SwitchTo.parent());
sally.attemptsTo(SwitchTo.parent());

12.8 问题允许参与者查询系统的状态

12.8 Questions allow an actor to query the state of the system

断言是任何测试的重要组成部分,而在 Screenplay 中,问题就是我们编写断言的方式。参与者可以回答有关系统状态的问题(见图 12.7),以便我们确定此状态是否符合我们的预期。

Assertions are an essential part of any test, and in Screenplay, questions are how we write our assertions. Actors can answer questions about the state of the system (see figure 12.7) so that we can decide whether this state is what we expect it to be.

图 12.7 演员可以利用他们的能力来回答有关系统状态的问题。

Figure 12.7 Actors can answer questions about the state of the system, using their abilities.

问题类使用参与者的能力从参与者的角度查询系统的状态。例如,如果参与者能够使用 WebDriver 与 Web 应用程序交互,Question则类可以使用此功能报告攻击者所查看的网页内容。让我们更详细地了解一下此功能的工作原理。

Question classes use the abilities of the actor to query the state of the system, from the perspective of the actor. For example, if an actor has the ability to use WebDriver to interact with a web application, Question classes can use this ability to report on the contents of the web pages that the actor sees. Let’s see how this works more closely.

12.8.1 查询系统状态的问题

12.8.1 Questions query the state of the system

认为我们需要在飞行常客应用程序的帐户页面上读取当前的积分余额(见图12.8)。

Suppose we need to read the current point balance on the account page of our Frequent Flyer application (see figure 12.8).

图 12.8 我们赋予演员使他们能够与系统交互的能力。

Figure 12.8 We give actors abilities that enable them to interact with the system.

就像我们之前看到的交互类一样,Screenplay 问题由类和对象表示。与交互类一样,Screenplay 库通常捆绑了一系列预定义的Question您可以使用它来查询您的应用程序状态。

Just like the interaction classes we saw earlier, Screenplay questions are represented by classes and objects. And just like interaction classes, Screenplay libraries typically come bundled with a range of predefined Question classes that you can use to query your application state.

在 Serenity BDD 中,查询状态级别的文本值的问题如下所示:

In Serenity BDD, a question to query the text value of the status level would look like this:

   Text.of(“[test-dataid='status-level']”)     
   Text.of("[test-dataid='status-level']")    

获取状态级元素的文本内容。

Get the text content of the status-level element.

班级Question带有一系列转换方法,您可以使用它们将值转换为所需的类型。例如,您可以使用以下代码以数值形式检索积分余额:

The Question class comes with a range of transformation methods that you can use to convert a value into the type you need. For example, you could use the following code to retrieve the point balance as a numerical value:

   Text.of(“[test-dataid='point-balance']”)。asInteger()     
   Text.of("[test-dataid='point-balance']").asInteger()    

将此文本值转换为整数。

Turn this text value into an integer.

Visibility或者我们甚至可以使用问题类来询问积分平衡是否可见像这样:

Or we could even ask whether the point balance is visible, using the Visibility question class like this:

Visibility.of(“[test-dataid='point-balance']”)
Visibility.of("[test-dataid='point-balance']")

所有这些代码片段都是 Screenplay 问题的示例。但它们本身并没有什么用。一个QuestionScreenplay 中的“制作视频”有点像在线投票:如果你不让任何人回答问题,你就不会获得太多信息。

All of these code snippets are examples of Screenplay questions. But none will do much by themselves. A Question class in Screenplay is a bit like an online poll: if you don’t ask anyone to answer the questions, you won’t get much information.

为了获取实际信息,我们需要让参与者回答问题。我们使用以下answeredBy()方法, 像这样:

To get the actual information, we need to ask an actor to answer the question. We do this with the answeredBy() method, like this:

String statusLevel = Text.of(“[test-dataid='status-level']”)     
                         .answeredBy(sally);                     
String statusLevel = Text.of("[test-dataid='status-level']")    
                         .answeredBy(sally);                    

查找状态级别字段的文本

Finds the text of the status-level field

在 Sally 的浏览器中看到

As seen in Sally’s browser

或者如果我们需要一个整数值,我们可以使用asInteger()方法我们之前看到过:

Or if we needed an integer value, we could use the asInteger() method we saw earlier:

   整数 pointBalance = Text.of(“[test-dataid='point-balance']”)     
                              .asInteger()                             
                              .answeredBy(sally);                      
   Integer pointBalance = Text.of("[test-dataid='point-balance']")    
                              .asInteger()                            
                              .answeredBy(sally);                     

我们想要点平衡元素的文本内容。

We want text content of the point balance element.

我们想将其用作整数值。

We want to use it as an integer value.

使用 Sally 在浏览器中看到的值

Uses the value Sally sees in her browser

如果我们想询问 Sally 是否看到了积分余额字段,并存储结果,我们可以这样写:

And if we wanted to ask Sally whether she sees the point balance field, and store the result, we could write something like this:

   布尔值 balanceIsVisible = Visibility.of(“[test-dataid='point-balance']”)
                                        .回答者(莎莉);                           
   boolean balanceIsVisible = Visibility.of("[test-dataid='point-balance']")
                                        .answeredBy(sally);                           

问题是获取有关应用程序状态信息的便捷方式。您可以查看一些最常用的 UIQuestion类的概述在 Serenity BDD 中表 12.3。

Questions are a convenient way to get information about the state of the application. You can see an overview of some of the more commonly used UI Question classes in Serenity BDD in table 12.3.

表 12.3 Serenity BDD 中常见的剧本问题类别

Table 12.3 Common Screenplay question classes in Serenity BDD

问题

Question

用法

Usage

例子

Example

Attribute

Attribute

查找给定元素的特定 HTML 属性值

Find a specific HTML attribute value for a given element

Attribute.of(".item-details-link")

Attribute.of(".item-details-link")

.named("href")

.named("href")

Disabled

Disabled

检查给定字段是否被禁用

Check whether a given field is disabled

Disabled.of("#login-button")

Disabled.of("#login-button")

Enabled

Enabled

检查给定字段是否已启用

Check whether a given field is enabled

Enabled.of("#login-button")

Enabled.of("#login-button")

SelectedValue

SelectedValue

下拉列表中选定元素的值

The value of the selected element in a dropdown list

SelectedValue.of("#travel-class")

SelectedValue.of("#travel-class")

Text

Text

元素的文本值

The text value of an element

Text.of("[test-dataid='name']")

Text.of("[test-dataid='name']")

Value

Value

表单字段的值(来自“value” CSS 属性)

The value of a form field (from the “value” CSS attribute)

Value.of("#firstName")

Value.of("#firstName")

Visibility

Visibility

检查元素在页面上是否可见

Check whether an element is visible on the page

Visibility.of(".error-message")

Visibility.of(".error-message")

12.8.2 特定领域的问题类使我们的代码更具可读性

12.8.2 Domain-specific Question classes make our code more readable

也很容易编写自己的Question课程,要么执行有关用户界面的更复杂的查询,要么查询被测系统的其他部分。有时,您可能只是为了提高可读性而编写自定义问题类。

It is also easy to write your own Question classes, either to perform more sophisticated queries about the user interface or to query other parts of the system under test as well. Sometimes, you might write a custom question class simply for better readability.

例如,我们了解了如何使用Text问题类来读取帐户页面上显示的状态级别(见图 12.8)。我们可以创建一个简单的问题,读取当前的积分余额并将其转换为整数值,如下所示:

For example, we saw how to read the status level displayed on the account page (see figure 12.8) using the Text question class earlier on. We could create a simple question that reads the current point balance and converts it to an integer value like this:

公共类帐户{
    公共静态问题<Integer> pointBalance() {
        返回 Text.of((“[test-dataid='point-balance']”).asInteger();
    }
}
public class Account {
    public static Question<Integer> pointBalance() {
        return Text.of(("[test-dataid='point-balance']").asInteger();
    }
}

我们现在可以直接以更易读的方式检索当前积分余额级别:

We can now retrieve the current point balance level directly in a more readable manner:

   int pointBalance = 帐户.pointBalance().answeredBy(sally);            
   int pointBalance = Account.pointBalance().answeredBy(sally);            

但是,我们还有另一种方式使用剧本模式的问题:对系统。

But there is another way we use questions using the Screenplay Pattern: to make assertions about the state of the system.

12.8.3 参与者可以使用问题来做出断言

12.8.3 Actors can use questions to make assertions

我们已经看到了Question课程可用于检索有关被测系统状态的信息。然后,我们可以使用此信息做出断言并描述我们期望的结果。例如,使用常规断言库,我们可以使用以下代码检查 Sally 的当前积分余额是否为 1,000:

We have seen how Question classes can be used to retrieve information about the state of the system under test. We can then use this information to make assertions and describe the outcomes we expect. For example, using a conventional assertion library, we could check that the Sally’s current point balance is 1,000 with the following code:

sally.attemptsTo(Click.on("#my-account"));                         
   int pointBalance = Account.pointBalance().answeredBy(sally);    
   断言.assertEquals(1000,po​​intBalance);                        
sally.attemptsTo(Click.on("#my-account"));                        
   int pointBalance = Account.pointBalance().answeredBy(sally);   
   Assert.assertEquals(1000, pointBalance);                       

打开“我的账户”页面

Opens the My Account page

检索 Sally 的当前积分余额

Retrieves the current point balance for Sally

检查它是否等于 1,000

Checks that it is equal to 1,000

或者,我们经常可以使用 Screenplay 库功能来使我们的断言更流畅、更易读。例如,使用 Serenity BDD,我们可以使用Ensure将我们的断言编织到正常的交互流程中,就像这样:

Alternatively, we can often use Screenplay library features to make our assertions more fluent and more readable. For example, with Serenity BDD we can use the Ensure class to weave our assertion into the normal flow of interactions, like this:

sally.尝试(
  Click.on(“#my-account”)                                  
  确保(Account.pointBalance()).isEqualTo(1000)      
(英文):
sally.attemptsTo(
  Click.on("#my-account")                                 
  Ensure.that(Account.pointBalance()).isEqualTo(1000)     
);

导航到“我的帐户”页面

Navigates to the My Accounts page

检查积分余额是否等于 1,000

Checks that the point balance is equal to 1,000

班级EnsureSerenity 中的 BDD 提供了一个流畅的接口来对问题或其他值做出断言,因此您获得的断言方法取决于您提出的问题的类型。例如,如果您提出一个返回String值的问题,我们可以做出String- 相关断言,而如果您提出一个返回整数的问题,我们可以做出数值断言。

The Ensure class in Serenity BDD provides a fluent interface to make assertions about questions or other values, so the assertion methods you get depend on the type of question you ask. For example, if you ask a question that returns a String value, we can make String-related assertions, whereas if you ask a question that returns an integer, we can make numerical assertions.

确保(Account.statusLevel())等于(UserLevel.BRONZE)       
Ensure.that(Account.statusLevel()).isEqualTo(UserLevel.BRONZE)       

其他 Screenplay 库也有类似的功能。例如,Serenity/JS 也有Ensure,但语法略有不同:

Other Screenplay libraries have similar capabilities. For example, Serenity/JS also has the Ensure class, though with a slightly different syntax:

确保(Account.pointBalance(,等于(1000))      
Ensure.that(Account.pointBalance(), equals(1000))      

班级Ensure是一个很好的例子,说明了 Screenplay 模式如何帮助我们使代码更具可读性和表现力,并更清楚地说明我们行为背后的商业意图。

The Ensure class is a good example of how the Screenplay Pattern helps us make our code more readable and expressive, and to illustrate the business intent behind our actions more clearly.

我们还了解了如何创建新的领域特定问题,以使我们的代码与领域更紧密相关。但如果我们可以对交互做同样的事情会怎样?如果我们可以将低级 UI 交互(如“单击提交”或“在 #firstname 字段中输入‘Jane’”)转变为更具业务意义的交互(如“注册新客户”或“输入个人信息”)会怎样?事实证明,使用任务的概念,我们可以做到这一点。让我们如何。

We also saw how we can create new domain-specific questions to make our code relate more closely to our domain. But what if we could do the same thing with interactions? What if we could turn low-level UI interaction, like “Click on Submit” or “Enter ‘Jane’ into the #firstname field,” into more business-meaningful ones, such as “Register new customer” or “Enter personal details”? It turns out we can, using the concept of tasks. Let’s see how.

12.9 任务模型化更高级别的业务操作

12.9 Tasks model higher-level business actions

我们我们已经看到参与者如何使用交互来调用被测系统并使用问题来查询其状态。但是,仅使用这种低级构造来表达我们的测试场景并不比直接调用 WebDriver 或 REST 客户端 API 有太大改进。它还可能导致冗长的测试,其中更高级别的业务特定概念难以辨别,并且很容易在参与者单击按钮和发送 HTTP 请求的噪音中被忽略。正如我们所见,Screenplay 模式提供了一种称为任务的构造对这些更高级别的抽象进行建模。本质上,任务表示参与者需要执行的一系列交互,以实现某些微目标(见图 12.9)。

We’ve seen how actors can invoke the system under test using interactions and query its state using questions. However, expressing our test scenarios using such low-level constructs alone would not be much of an improvement over doing so by directly calling WebDriver or REST client APIs. It could also lead to lengthy tests in which the higher-level business-specific concepts are difficult to discern and easy to miss in the noise of actors clicking on buttons and sending HTTP requests. And as we have seen, the Screenplay Pattern offers a construct called tasks to model those higher-level abstractions. In essence, tasks represent sequences of interactions that an actor needs to perform in order to achieve some micro goal (see figure 12.9).

图 12.9 任务允许我们将交互分组为有意义且可重复使用的块。

Figure 12.9 Tasks allow us to group interactions into meaningful and reusable blocks.

12.9.1 简单任务提高可读性

12.9.1 Simple tasks improve readability

一些任务可以非常简单,只需一次交互即可建模。为此,我们可以使用Task.where()方法,提供任务的描述以及组成该任务的交互或任务。

Some tasks can be incredibly simple and modeled using just one interaction. To do this, we can use the Task.where() method, providing a description of the task as well as the interactions or tasks that make up this task.

Open.url()例如,我们可能想要引入一个任务,让用户使用交互打开应用程序主页我们在本章前面看到过:

For example, we might want to introduce a task where the user opens the application home page using the Open.url() interaction we saw earlier in the chapter:

  公共类导航{
    公共静态可执行到TheHomePage(){
      return Task.where("{0} 进入登录页面",                       
                  打开.url(“https://www.frequent-flyers.bddinaction.com”)   
      (英文):
    }
  }
  public class Navigate {
    public static Performable toTheHomePage() {
      return Task.where("{0} goes to the login page",                      
                  Open.url("https://www.frequent-flyers.bddinaction.com")  
      );
    }
  }

任务的简短描述(“{0}”是参与者姓名的占位符)

A short description of the task ("{0}" is a placeholder for the name of the actor)

一个任务由一个描述和一个或多个交互或其他任务组成。

A task is made up of a description and one or more interactions or other tasks.

现在,我们的演员不需要打开特定的 URL,而是能够“导航到主页”:

Now, rather than opening a specific URL, our actors will be able to “Navigate to the home page”:

sally.尝试(
  导航到主页()
(英文):
sally.attemptsTo(
  Navigate.toTheHomePage()
);

看看这如何让我们更加关注演员想要实现的目标,而不是他们如何实现目标?引入上述这样的小任务可能看起来并不多,但它可以让我们获得遵循剧本模式的几个好处:

See how this allows us to focus more on what our actor is trying to achieve and less on how they make it happen? Introducing small tasks like the one above might not seem like much, but it allows us to reap several benefits of following the Screenplay Pattern:

  • 它帮助我们引入更高级别的抽象并封装可重用行为。现在,其他所有参与者也都可以导航到主页,而不必记住他们需要打开哪个 URL。

  • It helps us introduce higher-level abstractions and encapsulate bits of reusable behavior. Now every other actor will be able to navigate to the home page too, without having to remember what URL they need to open.

  • 任务,即使像所示的一样小,也可以帮助我们避免重复信息(例如 URL 和元素选择器),而是提供一个单一的地方来维护和更新这些详细信息。

  • Tasks, even as small as the one shown, help us avoid having to duplicate information such as URLs and element selectors and instead offer a single place to maintain and update these details.

  • 每个自定义任务都有自己的描述,这有助于生成比简单地列出所有“点击”和元素选择器更具信息的测试执行报告。

  • Each custom task has its own description, which helps to produce far more informative test execution reports than simply listing all the “clicks” and element selectors.

但这还不是全部。任务的主要目的不仅仅是提高可读性;它是为了让我们的代码更可重用,更不易损坏。让我们看看它是如何做到的作品。

But that’s not all. The main purpose of tasks is not simply to improve readability; it’s to make our code more reusable and less brittle. Let’s see how that works.

12.9.2 更复杂的任务增强了可重用性

12.9.2 More complex tasks enhance reusability

任务的真正威力在于,当我们将一系列其他交互和任务组合成可重用的业务功能块时。例如,假设我们希望我们的参与者能够打开登录页面,而无需明确提及主页。我们可以通过添加一个新toTheLoginPage()方法来实现对我们来说Navigate class就像这样:

The real power of tasks is when we combine a sequence of other interactions and tasks into a reusable chunk of business functionality. For example, suppose we wanted our actors to be able to open the login page without having to explicitly mention the home page. We could do this by adding a new toTheLoginPage() method to our Navigate class like this:

  公共类导航{
    ...
    公共静态最终由LOGIN_BUTTON 
      = By.xpath(“//button[normalize-space()='Login']”);     
        
    公共静态可执行到TheLoginPage(){
      return Task.where("{0} 进入登录页面",
              Navigate.toTheHomePage(),                      
              点击(LOGIN_BUTTON)                         
    (英文):
  }
  public class Navigate {
    ...
    public static final By LOGIN_BUTTON 
      = By.xpath("//button[normalize-space()='Login']");    
        
    public static Performable toTheLoginPage() {
      return Task.where("{0} goes to the login page",
              Navigate.toTheHomePage(),                     
              Click.on(LOGIN_BUTTON)                        
    );
  }

我们经常使用这样的常量作为定位器,以提高可读性。

We often use constants like this for locators to improve readability.

使用我们之前定义的 Navigate.toTheHomePage() 任务打开主页

Opens the home page using the Navigate.toTheHomePage() task we defined earlier

进入主页后,我们可以打开登录页面。

Once on the home page, we can open the login page.

一旦进入登录页面,我们需要输入用户的电子邮件和密码,然后单击登录按钮,才能访问应用程序。我们看到了如何使用低级交互来做到这一点,但我们也可以定义一个更可重用的任务来结合所有这些交互。它可能看起来像这样:

Once we get to the login page, we need to enter the user’s email and password, and click on the login button, to access the application. We saw how to do this using low-level interactions, but we could also define a much more reusable task that combines all these interactions. It might look something like this:

  公共类登录{
    公共静态可执行 usingCredentials (字符串用户名, 
                                               字符串密码){
      return Task.where("{0} 登录为 " + 用户名,
                Navigate.toTheLoginPage(),
                SendKeys.of(用户名).into(“#email”),
                SendKeys.of(密码).into(“#密码”),
                点击(“#login-button”)
      (英文):
    }
  }
  public class Login {
    public static Performable usingCredentials(String username, 
                                               String password) {
      return Task.where("{0} logs in as " + username,
                Navigate.toTheLoginPage(),
                SendKeys.of(username).into("#email"),
                SendKeys.of(password).into("#password"),
                Click.on("#login-button")
      );
    }
  }

这将使我们的测试用例的代码更加可读和可重用:

Which would lead to much more readable and reusable code for our test cases:

tracy.尝试(
  登录.使用凭据(“tracy@traveler.com”,“密码”)
tracy.attemptsTo(
  Login.usingCredentials("tracy@traveler.com","secretpassword")
;

可重用性是 Screenplay 模式的核心。在测试自动化中,我们经常从页面上交互的定位器和字段,或我们执行的低级交互的角度来考虑可重用性。

Reusability is at the heart of the Screenplay Pattern. In test automation, we often think of reusability in terms of the locators and fields we interact with on a page, or of the low-level interactions we perform.

但 Screenplay 将可重用性的概念发挥得更进一步。Screenplay 可以轻松定义由其他任务和交互组成的可重用任务,并使用这些任务执行测试和构建其他甚至更高级别的任务。

But Screenplay takes the concept of reusability much further. Screenplay makes it easy to define reusable tasks made up of other tasks and interactions, and to use these tasks both to execute tests and to build other, even higher-level tasks.

将所有这些元素结合在一起,我们最终会得到更简洁、更易读的测试代码,并且更清楚地反映业务流程和预期结果。我们可以在下面的代码中看到一个完整的示例:

Bringing all of these elements together, we end up with test code that is more succinct and more readable and that reflects the business flows and expected outcomes much more clearly. We can see a full example in the following code:

  WebDriver 驱动程序;
 
  @前
  公共无效打开浏览器(){
    driver = new ChromeDriver();                                           
  }
 
  @测试
  公共无效查看帐户余额(){
    演员 sally = Actor.named(“Sally”)
                       .whoCan(BrowseTheWeb.with(driver));                 
 
    sally.尝试(
      登录.usingCredentials(“sally@flying-high.com”,“密码”),    
 
      Navigate.toMyAccount()                                              
 
      确保(Account.pointBalance(),isEqualTo(1000)),                
      确保.that(Account.statusLevel()).isEqualTo(UserLevel.BRONZE)      。❺
    (英文):
 
    @后
    公共无效关闭浏览器(){
      驱动程序.退出();                                                       
    }
 
  }
  WebDriver driver;
 
  @Before
  public void openBrowser() {
    driver = new ChromeDriver();                                          
  }
 
  @Test
  public void viewAccountBalance() {
    Actor sally = Actor.named("Sally")
                       .whoCan(BrowseTheWeb.with(driver));                
 
    sally.attemptsTo(
      Login.usingCredentials("sally@flying-high.com","secretpassword"),   
 
      Navigate.toMyAccount(),                                             
 
      Ensure.that(Account.pointBalance(), isEqualTo(1000)),               
      Ensure.that(Account.statusLevel()).isEqualTo(UserLevel.BRONZE).     
    );
 
    @After
    public void closeBrowser() {
      driver.quit();                                                      
    }
 
  }

在任何测试开始前打开一个新浏览器

Opens a new browser before any test starts

将浏览器分配给 Sally

Assigns the browser to Sally

Sally 登录。

Sally logs on.

❹Sally查看她的帐户详细信息。

Sally views her account details.

我们检查是否显示了预期值。

We check that the expected values are displayed.

使用 @After 方法关闭浏览器,以确保即使测试失败也会关闭浏览器

Shuts down the browser in an @After method to ensure that it happens even if the test fails

但这与我们在前几章中看到的 Cucumber 场景有什么关系呢?让我们寻找出去!

But how does this relate to the Cucumber scenarios we’ve seen in the previous chapters? Let’s find out!

12.10 剧本和黄瓜

12.10 Screenplay and Cucumber

从目前看到的代码中,我们知道我们的参与者是谁。我们通过在测试代码中直接给参与者命名来创建参与者,如下所示:

In the code we have seen so far, we know who our actors are. We create our actors by giving them a name directly in our test code, like this:

    演员 tracy = Actor.named("Tracy");
    Actor tracy = Actor.named("Tracy");

但在 Cucumber 场景中,参与者的名称通常在场景文本中公布,而不是在测试代码中公布。例如,假设我们想要自动化一个关于我们在图 12.8 中看到的帐户页面的场景。假设我们在场景中使用预定义用户,这些用户在每次测试运行开始时设置。例如,Stan 是一个标准的常旅客会员,积分为零。

But in a Cucumber scenario, the name of the actor is often announced in the scenario text, not in the test code. For example, imagine we want to automate a scenario about the account page we saw in figure 12.8. Suppose we are using predefined users for our scenarios, who are set up at the start of each test run. For example, Stan is a standard Frequent Flyer member with zero points.

一个简单的场景来说明 Stan 如何检查他的帐户状态,如下所示:

A simple scenario to illustrate how Stan checks his account status might look like this:

场景:斯坦检查他的余额
  斯坦是标准常旅客会员,积分为 0 分
 
  鉴于斯坦已经登录了他的账户
  当他查看自己的账户详细信息时
  那么他的账户状态应该是:
    | 积分余额 | 身份等级 |
    | 0 | 标准 |
Scenario: Stan checks his balance
  Stan is a standard frequent flyer member with 0 points
 
  Given Stan has logged into his account
  When he views his account details
  Then his account status should be:
    | Point Balance | Status Level |
    | 0             | STANDARD     |

在这个场景中,Stan 是我们的演员,但在其他场景中,我们可能会与 Silvia(银卡常旅客会员)或 Bryony(铜卡会员)合作。换句话说,演员的名字需要是我们传递给场景的变量。由于演员的名字可以改变,我们需要在设置演员之前将名字传递给我们的步骤定义方法。

In this scenario, Stan is our actor, but in other scenarios we might work with Silvia (who’s a Silver Frequent Flyer member) or Bryony (a Bronze member). In other words, the name of the actor needs to be a variable that we pass to our scenario. Because the name of the actor can change, we need to pass the name to our step definition method before we set up our actor.

12.10.1 演员和阵容

12.10.1 Actors and casts

我们可以简单地传入演员的名字,然后使用Actor.named()方法创建一个新的演员我们之前看到过:

We could simply pass in the name of the actor and create a new actor using the Actor.named() method we saw earlier:

  @Given("{} 已登录其帐户")
  公共无效成员已登录到他们的帐户(字符串actorName){
    演员 actor = Actor.named(actorName);
    演员.可以(使用(司机)浏览网页);
    ...
  }
  @Given("{} has logged into his/her account")
  public void memberHasLoggedIntoTheirAccount(String actorName) {
    Actor actor = Actor.named(actorName);
    actor.can(BrowseTheWeb.with(driver);
    ...
  }

但是,与独立的 Screenplay 测试用例不同,参与者需要在其他步骤定义方法中使用,可能还需要在其他类中使用,所以我们需要在这些步骤之间跟踪参与者。

But, unlike a self-contained Screenplay test case, the actor will need to be used in other step definition methods, possibly in other classes, so we need to keep track of the actor between these steps.

幸运的是,Screenplay 库有一种方法可以让我们更轻松地完成这一任务:Cast。 Cast 允许我们管理参与 Screenplay 场景的演员。我们可以定义他们的能力并通过名字引用他们,这样我们就不需要跟踪Actor步骤之间的对象了。

Fortunately, Screenplay libraries have a way to make this easier for us: the Cast. A Cast allows us to manage the actors who take part in our Screenplay scenario. We can define their abilities and refer to them by name so that we don’t need to keep track of the Actor objects between steps.

OnStage.setTheStage()我们可以在场景开始使用方法之前设置一个 Cast我们还可以使用特殊OnlineCast给我们一个演员阵容,每个演员都配备了自己的WebDriver实例这样他们就可以使用我们之前看到的 WebDriver 交互类与应用程序进行交互章:

We can set up a Cast before our scenarios start using the OnStage.setTheStage() method. We can also use the special OnlineCast class to give us a Cast of actors, each equipped with their own WebDriver instance so that they can interact with the application using the WebDriver interaction classes we saw earlier in the chapter:

    @之前                                     
    公共无效设置舞台(){
      OnStage.setTheStage(new OnlineCast());     
    }
    @Before                                     
    public void setTheStage() {
      OnStage.setTheStage(new OnlineCast());    
    }

在执行每个 Cucumber 场景之前执行

Performs before each Cucumber scenario is executed

定义配备 WebDriver 实例的演员阵容

Defines a Cast of actors equipped with WebDriver instances

12.10.2 剧本阶段

12.10.2 The Screenplay stage

现在我们已经设置好了舞台,我们可以随时使用以下OnStage.theActorCalled()方法召唤演员

Now that we have set the stage, we can summon an actor any time we need one using the OnStage.theActorCalled() method:

  @Given("{} 已登录其帐户")
  公共无效成员已登录到他们的帐户(字符串actorName){
    演员 actor = OnStage.theActorCalled(actorName);
    演员.尝试(...);
  }
  @Given("{} has logged into his/her account")
  public void memberHasLoggedIntoTheirAccount(String actorName) {
    Actor actor = OnStage.theActorCalled(actorName);
    actor.attemptsTo(...);
  }

此场景的第二步是“当查看其帐户详细信息时”。我们没有明确命名我们的参与者;我们只是使用代词。Serenity BDD 和 Serenity/JS 等库将识别常见代词,因此以下步骤定义代码将假设“他”正在谈论 Stan,即此场景中提到的最后一个参与者:

The second step of this scenario reads “When he views his account details.” We don’t explicitly name our actor; we simply use a pronoun. Libraries like Serenity BDD and Serenity/JS will recognize common pronouns, so the following step definition code will assume that “he” is talking about Stan, the last actor referred to in this scenario:

@When("{} 查看他/她的帐户详细信息")
公共无效viewsAccountSummary(String actorName){
  OnStage.theActorCalled(actorName)。尝试(Navigate.toMyAccount());
}
@When("{} views his/her account details")
public void viewsAccountSummary(String actorName) {
  OnStage.theActorCalled(actorName).attemptsTo(Navigate.toMyAccount());
}

在某些情况下,我们需要间接引用参与者,并且该参与者的名称未在步骤中提及。在这种情况下,我们可以使用OnStage.theActorInTheSpotlight()方法,它将检索当前设想。

In some cases, we need to refer to an actor indirectly, and the name of the actor is not mentioned in the step. In that case, we can use the OnStage.theActorInTheSpotlight() method, which will retrieve the last active actor in the current scenario.

12.10.3 为参与者定义自定义参数类型

12.10.3 Defining a custom parameter type for actors

我们还可以通过为我们的演员定义自定义参数类型,使我们的 Screenplay 步骤定义代码更加简洁、更具表现力:

We can also make our Screenplay step definition code more concise and expressive by defining a custom parameter type for our actors:

@ParameterType(“.*”)
公共演员演员(字符串演员姓名){
    返回 OnStage.theActorCalled(actorName);
}
@ParameterType(".*")
public Actor actor(String actorName) {
    return OnStage.theActorCalled(actorName);
}

这样,在 Cucumber 场景中,可以通过名称(或代词)引用参与者,并自动转换为Actor步骤定义中的参数代码:

This way an actor can be referred to by name (or by pronoun) in the Cucumber scenario, and automatically converted into an Actor parameter in the step definition code:

@Given("{actor} 已登录其账户")
public void memberHasLoggedIn(Actor actor, String actorName) {     
  演员.尝试(...);                                           
}
@Given("{actor} has logged into his/her account")
public void memberHasLoggedIn(Actor actor, String actorName) {    
  actor.attemptsTo(...);                                          
}

现在将参与者作为参数传递到步骤定义中。

The actor is now passed into the step definition as a parameter.

因此,可以直接在步骤定义代码中使用参与者。

As a result, the actor can be used directly in the step definition code.

12.10.4 在枚举值中定义角色

12.10.4 Defining persona in enum values

有时我们需要了解场景中参与者的更多详细信息。例如,在上一个场景中,我们需要知道 Stan 的姓名、电子邮件地址和密码,以便他可以登录应用程序。如果其他场景使用其他参与者,我们也需要了解他们的详细信息。

Sometimes we need to know more details about the actors in our scenarios. For example, in the previous scenario, we need to know Stan’s name and his email address and password so that he can log on to the application. If other scenarios use other actors, we’ll need to know their details too.

有很多方法可以做到这一点,但一个非常简单的方法是用演员的名字及其凭证定义一个枚举:

There are many ways to do this, but one very simple approach is to define an enum with the names of the actors along with their credentials:

公共枚举 FrequentFlyer {
    斯坦(“ stan@flyinghigh.com”,“秘密”),
    Bryony("bryony@flyinghigh.com", "秘密"),
    西尔维娅(“silvia@flyinghigh.com”,“秘密”);
 
    公共最终字符串电子邮件;
    公共最终字符串密码;
    FrequentFlyer(字符串电子邮件,字符串密码){
        这个.电子邮件=电子邮件;
        这个.密码=密码;
    }
}
public enum FrequentFlyer {
    Stan("stan@flyinghigh.com", "secret"),
    Bryony("bryony@flyinghigh.com", "secret"),
    Silvia("silvia@flyinghigh.com", "secret");
 
    public final String email;
    public final String password;
    FrequentFlyer(String email, String password) {
        this.email = email;
        this.password = password;
    }
}

我们现在可以直接在步骤中引用这个枚举定义:

We can now refer to this enum directly in our step definition:

@Given("{} 已登录其帐户")
public void memberHasLoggedIntoTheirAccount(飞行常客会员) {
    theActorCalled(member.name()).attemptsTo(
            登录.使用凭据(会员.电子邮件,会员.密码)
    (英文):
}
@Given("{} has logged into his/her account")
public void memberHasLoggedIntoTheirAccount(FrequentFlyer member) {
    theActorCalled(member.name()).attemptsTo(
            Login.usingCredentials(member.email, member.password)
    );
}

12.10.5 Cucumber 中的剧本断言

12.10.5 Screenplay assertions in Cucumber

表演Cucumber 中的剧本断言与在独立场景中执行剧本断言没有什么不同,只是预期结果通常来自场景文本。例如,我们场景的最后一步如下:

Performing Screenplay assertions in Cucumber is no different than performing them in a self-contained scenario, except that the expected results generally come from the scenario text. For example, the final step of our scenario reads like this:

那么他的账户状态应该是:
  | 积分余额 | 身份等级 |
  | 0 | 标准 |
Then his account status should be:
  | Point Balance | Status Level |
  | 0             | STANDARD     |

与我们在前几章中看到的其他 Cucumber 自动化样式一样,我们可以定义一个@DataTypeType方法将表数据转换为更方便的域对象:

As with the other styles of Cucumber automation we have seen in previous chapters, we could define a @DataTypeType method to convert the table data into a more convenient domain object:

@DataTableType
公共帐户状态帐户状态(Map <String,String> statusValues){
    整数点数 = Integer.parseInt(statusValues.get("点数余额"));
    UserLevel level = UserLevel.valueOf(statusValues.get("状态级别"));
    返回新的AccountStatus(级别,积分);
}
@DataTableType
public AccountStatus accountStatus(Map<String, String> statusValues) {
    Integer points = Integer.parseInt(statusValues.get("Point Balance"));
    UserLevel level =  UserLevel.valueOf(statusValues.get("Status Level"));
    return new AccountStatus(level, points);
}

现在我们需要使用OnStage.theActorInTheSpotlight()方法检索我们的演员(因为他在场景中没有被直接引用)。 生成的步骤定义代码可能看起来像这样:

Now we need to use the OnStage.theActorInTheSpotlight() method to retrieve our actor (since he is not referred to directly in the scenario). The resulting step definition code might look something like this:

@Then(“他/她的账户状态应该包含:”)
public void accountStatusShouldContain(AccountStatus 预期) {
  OnStage.theActorInTheSpotlight().attemptsTo(
      确保(MyAccount.statusLevel())
            .isEqualTo(预期.userLevel()),
      确保(MyAccount.pointBalance())
            .isEqualTo(预期.pointBalance())
    (英文):
}
@Then("his/her account status should contain:")
public void accountStatusShouldContain(AccountStatus expected) {
  OnStage.theActorInTheSpotlight().attemptsTo(
      Ensure.that(MyAccount.statusLevel())
            .isEqualTo(expected.userLevel()),
      Ensure.that(MyAccount.pointBalance())
            .isEqualTo(expected.pointBalance())
    );
}

如您所见,使用 Screenplay 模式编写 Cucumber 步骤定义与编写自包含的 Screenplay 场景没有太大区别。唯一真正的区别是使用CastOnStage建立和管理设想演员。

As you can see, writing Cucumber step definitions using the Screenplay Pattern is not much different from writing self-contained Screenplay scenarios. The only real differences are the use of the Cast and OnStage classes to set up and manage the scenario actors.

概括

Summary

  • 您可以使用 Screenplay 模式编写可重复使用性更高、表达力更强的测试代码。Screenplay 是一种以参与者为中心的方法,参与者使用任务和交互与被测系统进行交互,并使用问题查询系统状态。

  • You can use the Screenplay Pattern to write more reusable and more expressive test code. Screenplay is an actor-centric approach where actors interact with the system under test using tasks and interactions and query the state of the system using questions.

  • 演员为我们的应用程序用户建模。

  • Actors model our application users.

  • 参与者具有能力,使他们能够以不同的方式与系统交互(例如,通过打开浏览器或调用 API)。

  • Actors have abilities, which allow them to interact with the system in different ways (e.g., by opening a browser or calling an API).

  • 参与者使用这些能力与应用程序进行交互。交互使我们能够将参与者的行为描述和建模为可重用对象的形式。

  • Actors use these abilities to perform interactions with the application. Interactions allow us to describe and model actor behavior into the form of reusable objects.

  • 参与者可以提出问题来查询系统的状态。我们利用问题做出断言,以确定应用程序的行为是否符合我们的预期。

  • Actors can ask questions to query the state of the system. We use questions to make assertions to determine whether the application behaves as we expect.

  • 我们可以将交互和任务组合成更高级别的任务,这样我们就可以在需要时在更高的抽象级别上推理我们的测试代码,但在必要时仍然能够查看与系统的详细交互。

  • We can combine interactions and tasks into higher-level tasks, allowing us to reason about our test code at a much higher level of abstraction when we need to, but still being able to look at detailed interactions with the system when necessary.

  • Screenplay 也很好地集成到了 Cucumber 中;通过对象动态提供演员Cast,并使用OnStage类在场景步骤之间进行管理。

  • Screenplay also integrates well into Cucumber; actors are provided dynamically, via a Cast object, and managed between scenario steps using the OnStage class.

在下一章中,我们将探讨这些原则以及 BDD 的一般概念如何应用于 API 和 Web 领域。服务。

In the next chapter, we will explore how these principles, and the concepts of BDD in general, apply to the world of APIs and web services.


1  从技术上讲,每个交互都实现了一个称为 performable 的特殊 Screenplay 接口,并且该attemptsTo()方法接受可执行对象列表作为参数。

1  Technically, each interaction implements a special Screenplay interface called performable, and the attemptsTo() method accepts a list of performable objects as parameters.

13 微服务和 API 的 BDD 和可执行规范

13 BDD and executable specifications for microservices and APIs

本章封面

This chapter covers

  • 基于 API 和微服务的架构以及如何测试它们
  • API and Microservice-based architectures and how to test them
  • 更广泛的用户旅程要求与更具体的业务规则要求之间的区别,以及如何使用 API 交互来自动化用户旅程要求
  • The difference between broader user journey requirements and more specific business rule requirements, and how user journey requirements can often be automated using API interactions
  • 使用 RESTAssured 与 REST API 交互
  • Using RESTAssured to interact with REST APIs
  • 使用 JSONPath 查询 REST API 调用返回的响应
  • Using JSONPath to query the responses returned by REST API calls

到目前为止,我们一直专注于自动化测试用户界面。但是,正如我们在第 10 章中讨论的那样,用户界面测试不应该是自动化验收测试工具箱中的唯一工具。知道何时使用它们以及何时寻找替代策略很重要。在本章中,您将了解不涉及用户界面的其他自动化验收测试方法。

Thus far we have focused on automating tests that exercise the user interface. But, as we discussed in chapter 10, user interface tests shouldn’t be the only tool in your automated acceptance testing toolbox. It’s important to know when to use them and when to look for alternative strategies. In this chapter, you’ll learn about other ways to automate your acceptance tests that don’t involve exercising the user interface.

在第 10 章和第 11 章中,您学习了如何使用自动化 Web 测试工具(如 Selenium WebDriver)来自动化验收测试。Web 测试是模拟用户在系统中的旅程、说明与应用程序的交互以及记录应用程序关键功能的绝佳方式。利益相关者可以轻松与 Web 测试产生共鸣,因为它们非常直观且与用户体验紧密相关。自动化 Web 测试还可用于演示新功能,这是增强测试信心的好方法。自动化 Web 测试也是有效测试直接在用户界面 (UI) 中实现的业务逻辑的唯一方法。

In chapters 10 and 11, you learned how to automate acceptance tests using automated web testing tools such as Selenium WebDriver. Web tests are a great way to simulate the user’s journey through the system, to illustrate interactions with an application, and to document the key features of your application. Stakeholders can easily relate to web tests, as they’re highly visual and intuitive and map closely to the user experience. Automated web tests can also be used to demonstrate new features, which is a great way to increase confidence in the tests. Automated web tests are also the only way to effectively test business logic that’s implemented directly within the user interface (UI).

但您也看到,端到端 Web 测试的执行速度往往比不涉及 UI 的测试慢得多。与浏览器交互会在执行时间和系统资源方面增加大量开销。依赖于真实浏览器的 Web 测试也更容易受到难以控制的技术或环境相关问题的影响。例如,Web 测试可能会因为测试机器上安装了错误版本的浏览器、网站响应缓慢或浏览器崩溃而失败。由于与应用程序逻辑无关的原因而失败的测试会浪费开发时间和资源,并会降低团队对测试套件的信心。

But you also saw that end-to-end web tests tend to execute significantly more slowly than tests that don’t involve the UI. Interacting with a browser adds significant overhead in terms of execution time and system resources. Web tests that rely on a real browser are also more subject to technical or environment-related problems that are hard to control. For example, web tests can fail because the wrong version of a browser is installed on a test machine, because the site is responding slowly, or because the browser crashes. Tests that fail for reasons unrelated to the application logic waste development time and resources and can reduce the team’s confidence in the test suite.

尽管它们具有很大的价值,但在自动化 BDD 场景时,Web 测试不应该是你的唯一选择:

Although they can have great value, web tests shouldn’t be your only option when it comes to automating your BDD scenarios:

  • 大多数应用程序需要合理结合自动化 UI 测试和非 UI 组件的自动化测试。

  • Most applications need a judicious mix of both automated UI tests and automated tests for non-UI components.

  • 非 UI 测试可以在应用程序的不同级别工作,包括直接执行应用程序代码中实现的业务规则的测试和与远程服务一起工作的测试。

  • Non-UI tests can work at different levels of the application, including tests that exercise the business rules implemented in the application code directly and tests that work with remote services.

  • 非 UI 测试还可用于验证非功能性需求,例如性能。

  • Non-UI tests can also be used to verify nonfunctional requirements, such as performance.

  • 实施非 UI 验收测试和相应的应用程序代码也是发现应用程序所需组件以及在应用程序中设计干净、有效的 API 的好方法。

  • Implementing non-UI acceptance tests, and the corresponding application code, is also a great way to discover what components your application needs and to design clean, effective APIs within your application.

理解 BDD 场景在微服务架构中的作用也很重要,微服务架构是大型应用程序越来越流行的方法,而这些架构的构建方式在我们如何指定和测试在其上运行的功能方面可以发挥重要作用。

It is also important to understand the role BDD scenarios play in microservice architectures, which are an increasingly popular approach for large applications, and the way these architectures are built can play a major role in how we specify and test features designed to run on them.

让我们首先了解一下 API 和微服务的含义,以及如何在您的项目和验收测试中找到 UI 和非 UI 验收测试技术之间的正确平衡。

Let’s start with looking at what APIs and microservices are all about and how you should find the correct balance between UI and non-UI acceptance testing techniques for your projects and acceptance tests.

13.1 API 及其测试方法

13.1 APIs and how to test them

API是应用程序编程接口的缩写;API 描述了应用程序如何以编程方式与另一个应用程序交互。换句话说,API 定义了如何编写代码来与某些底层应用程序或服务交互。它描述了您可以执行的操作、可以提出的查询以及可以发送到应用程序和从应用程序接收的数据结构。您可能遇到的常见 API 架构包括面向服务的架构 (SOA)) 和微服务。1

API is short for Application Programming Interface ; an API describes how an application can interact with another application programmatically. In other words, an API defines how you can write code to interact with some underlying application or service. It describes the actions you can perform, the queries you can ask, and the data structures you can send to and receive from the application. Common API architectures that you are likely to encounter include service-oriented architectures (SOA) and microservices.1

使用 API 测试应用程序比通过用户界面测试更技术化,需要对应用程序架构有更深入的了解,但它提供了比单独使用 UI 更快、更强大、更全面的测试的可能性。为了让您了解 API 测试如何提供帮助,我们将介绍一些常见的场景,在这些场景中,API 测试可以帮助编写自动化验收测试更轻松。

Testing applications using APIs is a bit more technical and requires a deeper understanding of the application architecture than testing via the user interface, but it opens up the possibility of faster, more robust, and more comprehensive testing than we could do with the UI alone. To give you a feel for how API testing can help, we will look at a number of common scenarios where API testing can help make writing automated acceptance tests easier.

13.2 使用 Web UI 和微服务定义功能

13.2 Defining a feature using a web UI and a microservice

在本部分中,您将学习如何为涉及单个微服务的简单需求编写可执行规范。当旅行者注册为常旅客时,他们会打开 Flying High 网站并导航到注册页面。或者,他们可能点击了营销部门发送的电子邮件中的链接。在这两种情况下,他们最终都会进入注册页面,在那里输入他们的详细信息。

In this section, you will learn how to write an executable specification for a simple requirement involving a single microservice. When a traveler registers as a frequent flyer, they open the Flying High website and navigate to the registration page. Alternatively, they might have clicked on a link in an email sent by the marketing department. In both cases, they end up on a registration page, where they enter their details.

一旦他们输入了详细信息,他们就会收到一封电子邮件,其中包含一个链接,他们可以单击该链接来确认他们的电子邮件地址;这将激活他们的帐户并允许他们登录并访问他们的帐户详细信息。此功能的一般架构如图 13.1 所示。

Once they have entered their details, they are sent an email with a link they can click to confirm their email address; this activates their account and allows them to log in and access their account details. The general architecture for this feature is illustrated in figure 13.1.

图 13.1 飞行常客会员服务提供了一个 REST API,允许飞行常客查看他们的积分历史记录并在首次注册时确认他们的电子邮件地址。

Figure 13.1 The Frequent Flyer membership service presents a REST API that allows a frequent flyer to view their points history and to confirm their email address when they first register.

13.2.1 理解要求

13.2.1 Understanding the requirements

开始工作后,开发团队成员与市场营销部门的 Marcus 聚在一起,更详细地讨论这一要求。这个功能似乎很简单,但始终值得确保每个人都达成共识。

Before they start work, the development team members get together with Marcus from marketing to run through this requirement in more detail. This feature seems straightforward enough, but it’s always worth making sure everyone is on the same page.

在快速的示例映射会议之后,团队提出了一组业务规则和示例(见图 13.2),并发现了以下内容细节:

After a quick Example Mapping session, the team comes up with a set of business rules and examples (see figure 13.2) and uncovers the following details:

  • 该功能的主要目的是允许旅行者在 Flying High 网站上注册成为常旅客。

  • The primary goal of the feature is to allow travelers to sign up as frequent fliers on the Flying High website.

  • 新的常旅客需要提供他们的姓名、地址和电子邮件。

  • New frequent flyers need to provide their name, address, and email.

  • 如果使用给定电子邮件的帐户已经存在,则应向用户提供重置密码的选项。

  • If an account already exists with a given email, the user should be presented with the option to reset their password.

  • 拥有有效的电子邮件尤为重要,因为 Marcus 希望向会员发送促销和营销材料。因此,新的常旅客必须先验证他们的电子邮件,然后才能激活他们的帐户。

  • Having a valid email is particularly important, as Marcus wants to send members promotions and marketing material. For this reason, new frequent flyers will have to validate their email before their account can be activated.

  • 合规和法律部门希望新的常旅客在加入之前同意条款和条件。

  • Compliance and legal want new frequent flyers to approve the terms and conditions before joining.

  • 这些验证电子邮件可能会丢失或被忽视,因此如果电子邮件地址在一定时间后未得到验证,则应自动发送后续电子邮件。

  • These validation emails can get lost or overlooked, so if an email address doesn’t get validated after a certain amount of time, a follow up email should be automatically sent out.

  • 当会员注册时,他们会收到一封欢迎电子邮件。营销部门使用专门的软件来管理邮件的详细信息和顺序。

  • When a member signs up, they should receive a welcome email message. The details and sequencing of the message are managed by the marketing department using specialized software.

图 13.2 注册特征的示例地图

Figure 13.2 The Example Map for the registration feature

13.2.2 从需求到可执行规范

13.2.2 From requirements to executable specifications

通过这些规则和示例,我们可以定义一组可执行规范来捕获这些规则的意图。但是,我们之前讨论的需求是高级用户旅程(例如,“旅行者可以注册为新成员”),更细粒度的需求(例如,“旅行者必须在加入前同意条款和条件”或“旅行者必须提供有效的电子邮件地址”),以及涉及可能超出团队范围的系统的需求(例如,“新成员加入时应收到欢迎电子邮件”)。让我们仔细看看我们通常遇到的不同类型的验收标准,以及如何使用 Gherkin 表达它们。

From these rules and examples, we can define a set of executable specifications that capture the intent of these rules. However, the requirements we discussed earlier are a mixture of high-level user journeys (e.g., “Travelers can register as new members”), more granular requirements (e.g., “Travelers must approve the terms and conditions before joining” or “A traveler must provide a valid email address”), and requirements that involve systems potentially outside the team’s scope (e.g., “New members should receive a welcome email when they join”). Let’s take a closer look at the different kinds of acceptance criteria we typically come across and how we can express them using Gherkin.

可执行规范为我们提供了全局

Executable specifications that give us the big picture

在深入研究细节之前,将关键业务成果以几个简洁易读的场景来描述始终是一个好主意。这有助于我们更清楚地记录功能的目的,并将重点放在真正重要的业务成果上。

It is always a good idea to capture the key business outcomes as a few succinct, easy-to-read scenarios before drilling more into the details. This helps us document the intent of a feature more clearly and keep the focus on the business outcomes that really matter.

图 13.2 中的第一个示例就是一个很好的例子:它代表了此功能旨在提供的整体业务结果。我们可以为这个例子写一个详细的场景,如下所示:

The first example in figure 13.2 is a good example of this: it represents the overall business outcome that this feature is meant to provide. We might write a detailed scenario for this example along the following lines:

规则:旅客可以在 Flying High 网站上注册成为新会员
 
  示例:Tracy 注册成为新的常旅客
    鉴于Tracy 没有飞行常客账户
    她使用有效信息注册新的飞行常客账户
时    她确认了她的电子邮件地址
    那么她应该有一个新的标准等级账户,积分为 0
Rule: Travelers can register as new members on the Flying High website
 
  Example: Tracy registers as a new Frequent Flyer
    Given Tracy does not have a Frequent Flyer account
    When she registers for a new Frequent Flyer account with valid details
    And she confirms her email address
    Then she should have a new Standard tier account with 0 points

此类场景让我们大致了解了整个用户旅程以及用户需要与之交互的 UI 的不同部分。它们可以针对已部署的、正在运行的应用程序进行测试(尽管理想情况下,开发人员也应该能够在本地运行它们)。

Scenarios like this give us an overview of the overall user journey and the different parts of the UI a user will need to interact with. They can be tested against a deployed, running application (though ideally a developer should be able to run them locally as well).

这个场景可以用基于 UI 的场景来演示,就像我们在前面章节中看到的那样。然而,仔细观察后,我们可能会注意到这些步骤都不需要与 UI 交互。我们可以直接与会员服务交互来注册新的飞行常客帐户,使用服务器提供的链接确认电子邮件地址,还可以检查新帐户的存在和状态。

This scenario could be demonstrated with a UI-based scenario, like those we have seen in the previous chapters. However, upon closer observation, we might note that none of these steps need to interact with the UI. We can interact directly with the membership service to register a new Frequent Flyer account, to confirm the email address using the link the server provides, and also to check the existence and status of the new account.

换句话说,对于像这样的业务旅程场景,我们可以选择自动化 UI 交互或执行一系列 API 交互。如果我们有其他更详细地探索 UI 的场景,则尤其如此。在本章后面,我们将看到如何在不使用 UI 的情况下自动化这样的场景測試。

In other words, for business journey scenarios like this one, we have the option of automating the UI interactions or of performing a sequence of API interactions. This is especially true if we have other scenarios that explore the UI in more detail. Later in the chapter, we will see how we could automate a scenario like this without using UI tests.

探索用户旅程的可执行规范

Executable specifications that explore the user journey

现在既然我们有了一个可以说明大局的场景,我们就可以开始更详细地探索用户旅程。例如,第二条规则(“旅行者必须在激活帐户之前确认其电子邮件”)分解了注册和验证电子邮件地址的过程。我们可以用一系列较小的场景来记录这个过程,如下所示(有关本章的源代码,请参阅http://mng.bz/710m):

Now that we have a scenario that illustrates the bigger picture, we can start to explore the user journey in more detail. For example, the second rule (“Travelers must confirm their email before their account is activated”) breaks down the process of registering and validating the email address. We could document this process with a series of smaller scenarios, like the following (for this chapter’s source code, see http://mng.bz/710m):

规则:旅行者必须在创建帐户前确认其电子邮件
  
  例如:Tracy 的帐户最初处于待激活状态
    鉴于Tracy 没有飞行常客账户
    Tracy 注册新的飞行常客账户
时    然后她应该收到一封带有电子邮件验证链接的电子邮件
    她的帐户应等待激活
 
  例如:Tracy 在激活账户之前无法访问自己的账户 
电子邮件
    鉴于Tracy 已注册新的飞行常客账户
    她尚未确认她的电子邮件
    她尝试访问她的飞行常客帐户详细信息时
    然后应该邀请她首先确认她的电子邮件地址
 
  例如:Tracy 确认了她的电子邮件
    鉴于Tracy 已注册新的飞行常客账户
    她确认她的电子邮件地址时
    然后她的帐户应该被激活
Rule: Travelers must confirm their email before their account is created
  
  Example: Tracy's account is initially pending activated
    Given Tracy does not have a Frequent Flyer account
    When Tracy registers for a new Frequent Flyer account
    Then she should be sent an email with an email validation link
    And her account should be pending activation
 
  Example: Tracy cannot access her account before having activated her 
 email
    Given Tracy has registered for a new Frequent Flyer account
    But she has not yet confirmed her email
    When she attempts to access her Frequent Flyer account details
    Then she should be invited to first confirm her email address
 
  Example: Tracy confirms her email
    Given Tracy has registered for a new Frequent Flyer account
    When she confirms her email address
    Then her account should be activated

请注意,在第三个场景中,我们没有添加“她应该能够登录”这样的子句来说明她确认电子邮件地址的效果。虽然我们可以添加此步骤,但这并不是绝对必要的(我们已经在前面看到的“Tracy 注册为新常旅客”场景中演示了用户可以在确认电子邮件后登录),并且添加额外的步骤会使场景变慢并且可能不太稳定。

Note that in the third scenario we have not included a clause like “And she should be able to log in” to illustrate the effect of her confirming her email address. While we could include this step, it is not strictly necessary (we have already demonstrated that a user can log in after confirming their email in the “Tracy registers as a new Frequent Flyer” scenario we saw earlier), and adding the extra step would make the scenario slower and potentially less stable.

我们还可以探索更详细的场景,例如反面例子,“提供的电子邮件是正确的”。从表面上看,这听起来像是一个简单的字段格式检查,但营销部门的 Marcus 非常重视为他的营销活动获取高质量的潜在客户。因此,除了基本的语法检查外,Marcus 还希望确保电子邮件来自有效的域,而不是一次性电子邮件地址。

We could also explore more detailed scenarios, such as the negative example, “the one where the provided email is correct.” On the surface of it, this sounds like a simple field format check, but Marcus from marketing places a lot of value on acquiring good-quality leads for his marketing campaigns. So, in addition to basic syntax checking, Marcus wants to make sure the emails come from valid domains and are not disposable email addresses.

这些要求可以通过一些有代表性的例子来说明,如下所示:

These requirements could be illustrated by a few representative examples, like the ones shown here:

在 API 级别实现的细粒度业务规则
 
规则:旅客必须提供有效的电子邮件地址
  场景概述:电子邮件必须格式正确且不可丢弃
    鉴于Tracy 没有飞行常客账户
    她使用 <email> 邮箱注册时
    然后电子邮件地址应为<Accepted/Rejected>,并显示消息“<Reason>”
 
    示例:应接受格式正确的电子邮件
      | 电子邮件 | 接受/拒绝 | 原因 |
      | sarah@example.org | 已接受 | |
      | sarah-jane@example.org | 已接受 | |
 
    示例:无效的电子邮件应被拒绝
      | 电子邮件 | 接受/拒绝 | 原因 |
      | example.com | 已拒绝 | 电子邮件必须是电子邮件 |
      | #@%^%#$@#$@#.com | 已拒绝 | 电子邮件必须是电子邮件 |
      | email@example..com | 已拒绝 | email 必须是电子邮件 |
      | email@inexistant-domain.com | 已拒绝 | 电子邮件地址无效 |
A granular business rule implemented at the API level
 
Rule: Travelers mush provide a valid email address
  Scenario Outline: Emails must be correctly formed and non-disposable
    Given Tracy does not have a Frequent Flyer account
    When she registers with an email of <email>
    Then the email address should be <Accepted/Rejected> with the message "<Reason>"
 
    Examples: Correctly-formed emails should be accepted
      | email                  | Accepted/Rejected | Reason |
      | sarah@example.org      | Accepted          |        |
      | sarah-jane@example.org | Accepted          |        |
 
    Examples: Invalid emails should be rejected
      | email                       | Accepted/Rejected | Reason                 |
      | example.com                 | Rejected          | email must be an email |
      | #@%^%#$@#$@#.com            | Rejected          | email must be an email |
      | email@example..com          | Rejected          | email must be an email |
      | email@inexistant-domain.com | Rejected          | Invalid email address  |

用户将在注册页面的“原因”列中看到错误消息,就像其他字段验证错误一样。但是,实际的电子邮件验证是在服务器上执行的,因此此场景可以通过 API 调用来实现,而无需参考用户界面。

The user will see the error message mentioned in the Reason column on the registration page, just like other field validation errors. However, the actual email validation is performed on the server, so this scenario could be implemented via API calls without needing to refer to the user interface.

专注于 UI 行为的可执行规范

Executable specifications that focus on UI behavior

其他场景可以在更独立的环境中进行测试。例如,第二个示例中的大多数字段验证规则都可以使用 UI 组件测试单独进行测试。我们可以使用更细粒度的场景来实现这些规则,如下所示:

Other scenarios can be tested in a more isolated context. For example, most of the field validation rules in the second example could be tested in isolation using a UI component test. We might implement these with more granular scenarios like the following:

规则:新会员需要填写所有必填字段并批准 
条款和条件
  场景:Candy 未能输入必填字段
    鉴于Candy 没有飞行常客账户
    Candy 想要注册新的飞行常客账户
时    那么注册时必须提供以下信息:
      | 电子邮件 |
      | 密码 |
      | 名字 |
      | 姓氏 |
      | 地址 |
      | 国家 |
Rule: New members need to complete all the mandatory fields and approve the 
 terms & conditions
  Scenario: Candy fails to enter to enter a mandatory field
    Given Candy does not have a Frequent Flyer account
    When Candy wants to register a new Frequent Flyer account
    Then the following information should be mandatory to register:
      | email     |
      | password  |
      | firstName |
      | lastName  |
      | address   |
      | country   |

与批准条款和条件相关的要求类似:

The requirements related to approving the terms and conditions are similar:

例如:Tracy 忘记同意条款和条件
    鉴于Tracy 没有飞行常客账户
    Tracy 试图在未同意条款和条件的情况下进行注册时
    然后她应该被告知“请确认条款和条件 
继续”
Example: Tracy forgets to agree to the Terms and Conditions
    Given Tracy does not have a Frequent Flyer account
    When Tracy tries to register without approving the terms and conditions
    Then she should be told "Please confirm the terms and conditions to 
 continue"

其他场景可能专注于 UI 交互,但使用 API 服务来设置测试数据并验证结果。例如,当用户尝试使用已存在的电子邮件进行注册时,应该向他们提供重置密码的选项。这里的核心业务需求是避免多个帐户,因此我们可以想出一个更丰富多彩的例子,如下所示:

Other scenarios might focus on UI interactions but use API services to set up test data and verify the outcomes. For example, when a user tries to register with an email that already exists, they should be presented with the option to reset their password. The core business need here is to avoid multiple accounts, so we could come up with a more colorful example like this one:

规则:不允许使用相同电子邮件地址的重复帐户
 
  例如:有人试图用已经使用过的电子邮件进行注册
 
  Trevor 是现有的飞行常客计划会员。
  他的妻子 Candy 没有飞行常客账户
 
    鉴于Harry Smith 已注册为 thesmiths@example.org
    Candy 尝试使用同一个电子邮件注册时
    然后她应该被告知该电子邮件地址已被使用
    应该可以选择重置密码
Rule: Duplicate accounts with the same email address are not allowed
 
  Example: Someone tries to register with an email that is already used
 
  Trevor is an existing Frequent Flyer member.
  His wife Candy does not have a Frequent Flyer account
 
    Given Harry Smith has registered as thesmiths@example.org
    When Candy tries to register with the same email
    Then she should be informed that this email address is already in use
    And she should be presented with the option to reset her password

这是一个涉及 API 和 UI 交互的场景的很好示例。在这个场景的第一步中,我们了解到 Harry 已经注册了一个飞行常客帐户。此步骤通常通过 API 调用来实现。我们已经说明了用户如何与注册页面交互以进行注册,在这里重复这些交互没有什么意义。

This is a good example of a scenario that involves both API and UI interactions. In the first step of this scenario, we learn that Harry has already registered for a Frequent Flyer account. This step would typically be implemented via an API call. We have already illustrated how a user interacts with the registration page to register, and there is little value in repeating those interactions here.

但是,以下步骤说明了 Candy 如何尝试输入相同的电子邮件,但不允许注册;相反,她可以选择重置密码。这是一个非常注重 UI 的流程,Candy 填写注册表单并被重定向到相应的密码重置页。

However, the following steps illustrate how Candy tries to enter the same email but is not allowed to register; instead, she is proposed with the option to reset her password. This is a very UI-focused flow, with Candy completing the registration form and being redirected to the appropriate password reset page.

说明系统如何交互的可执行规范

Executable specifications that illustrate how systems interact

其他场景可能需要与外部服务交互。例如,图 13.2 中的第二条业务规则讨论了新常旅客在激活其帐户之前需要验证其电子邮件地址的方式。我们可以使用以下场景在 Gherkin 中表达此业务规则:

Other scenarios may need to interact with external services. For example, the second business rule in figure 13.2 discusses how a new Frequent Flyer needs to validate their email address before their account can be activated. We might express this business rule in Gherkin with scenarios along the following lines:

规则:旅行者必须在创建帐户前确认其电子邮件
  例如:Tracy 的帐户最初处于待激活状态
    鉴于Tracy 没有飞行常客账户
    Tracy 注册新的飞行常客账户
时    然后她应该收到一封带有电子邮件验证链接的电子邮件
    的帐户应等待激活
 
    例如:Tracy 在激活账户之前无法访问自己的账户 
电子邮件
      鉴于Tracy 已注册新的飞行常客账户
      还没有确认她的电子邮件
      然后,她应该被邀请确认她的电子邮件地址,当她 
尝试登录
 
  例如:Tracy 确认了她的电子邮件
    鉴于Tracy 已注册新的飞行常客账户
    她确认她的电子邮件地址时
    然后她的帐户应该被激活
Rule: Travelers must confirm their email before their account is created
  Example: Tracy's account is initially pending activated
    Given Tracy does not have a Frequent Flyer account
    When Tracy registers for a new Frequent Flyer account
    Then she should be sent an email with an email validation link
    And her account should be pending activation
 
    Example: Tracy cannot access her account before having activated her 
 email
      Given Tracy has registered for a new Frequent Flyer account
      And she has not yet confirmed her email
      Then she should be invited to confirm her email address when she 
 attempts to login
 
  Example: Tracy confirms her email
    Given Tracy has registered for a new Frequent Flyer account
    When she confirms her email address
    Then her account should be activated

在这些情况下,让 Tracy 通过 UI 注册会很慢而且浪费资源(因为我们已经在其他地方演示过该用户旅程)。对于这两种情况,通过 API 调用直接注册 Tracy 的帐户会更高效。

Having Tracy register through the UI for these scenarios would be slow and wasteful (since we have already illustrated that user journey elsewhere). It would be much more efficient to register Tracy’s account directly via an API call for both these scenarios.

现在假设电子邮件是由我们团队范围之外的单独服务发送的。亲自检查 Tracy 是否在她的收件箱中收到了确认电子邮件是低效的,尤其是当电子邮件服务不在我们的工作范围内,或者可能是由另一家公司开发的完全外部系统时。相反,我们可以简单地确保正确的消息被发送到电子邮件服务。我们将看到这在下一个部分。

Now suppose that emails are sent by a separate service, outside of the scope of our team. It would be inefficient to physically check whether Tracy received her confirmation email in her inbox, especially if the email service is not in our scope of work or perhaps is a completely external system developed by a separate company. Instead, we can simply ensure that the correct message is sent to the email service. We’ll see how this could work in the next section.

13.3 微服务自动化验收测试

13.3 Automating acceptance tests for microservices

作为我们已经看到,即使是一个简单的功能也会导致一系列的要求和验收标准。而且很多时候,这些验收标准中的许多都可以自动化和验证,而几乎不需要或根本不需要参考 UI。

As we have seen, even a simple feature can lead to a range of requirements and acceptance criteria. And very often, many of these acceptance criteria can be automated and validated with little or no reference to the UI.

当我们自动化场景时,我们希望以最有效的方式逐案进行。有些场景可能需要 UI 交互,而其他场景可能完全通过 API 调用实现,而其他场景可能需要 UI 和 API 调用的组合。这很好。此阶段的重点不是测试应用程序的特定层;而是说明应用程序在业务方面应该做什么。一旦我们了解了应用程序应该做什么,以及哪些行为示例可以让我们相信该功能按预期执行,我们就可以考虑如何演示这些例子。

When we automate scenarios, we want to do so in the most effective way possible, on a case-by-case basis. Some scenarios might require UI interactions, whereas others might be possible entirely via API calls, while still others might need a combination of UI and API calls. And that’s fine. The focus at this stage is not to test a specific layer of the application; it is to illustrate what the application should do in business terms. Once we understand what the application should do, and what examples of behavior will give us confidence that the feature is performing as expected, we can consider how we will go about demonstrating these examples.

API 测试或使用 API 进行测试

API testing, or testing with APIs

传统的测试自动化思维鼓励我们从应用层的角度来思考:端到端测试、UI 测试、API 测试、集成测试、单元测试等等。每个层使用的测试库和技术之间有明确的区分。

Traditional thinking about test automation encourages us to think in terms of application layers: end-to-end tests, UI tests, API tests, integration tests, unit tests, and so forth. There is a clear segregation between the testing libraries and techniques used for each layer.

然而,当我们编写和自动化可执行规范时,我们的思维需要有所不同。目标是说明和自动化业务用例或业务规则的示例,而不是执行应用程序的特定层,例如 UI 或 API。

However, when we are writing and automating executable specifications, our thinking needs to be a little different. The goal is to illustrate and automate an example of a business use case or business rule, not to exercise a specific layer of the application, such as the UI or an API.

换句话说,虽然我们的自动化测试与系统的接口(包括 UI 和 API 组件)交互,但它们的目的不是测试这些接口本身。它们的目标是确保我们的系统符合给定的业务场景并且业务规则成立。与接口交互只是一种手段,而不是目标本身。

In other words, while our automated tests interact with the interfaces of our system, including UI and API components, their purpose is not to test those interfaces themselves. Their goal is to ensure that our system sports a given business scenario and that business rules hold. Interacting with the interfaces is just a means to an end rather than a goal on its own.

13.4 正在测试的微服务架构

13.4 The microservice architecture under test

本节中,我们将通过一个示例来介绍如何仅使用 API 交互来自动化迄今为止看到的场景。让我们首先仔细看看飞行常客会员 API,如图 13.3 所示。

In this section, we walk through an example of how we can automate the scenarios we have seen so far, using only API interactions. Let’s start by taking a closer look at the Frequent Flyer membership APIs, which are illustrated in figure 13.3.

图 13.3 飞行常客会员结构

Figure 13.3 The Frequent Flyer membership architecture

当新的常旅客在注册页面 (1) 上注册时,会向常旅客端点发送一个 POST 请求,其中包含有关新会员的详细信息 (2)。常旅客服务会在会员数据库中添加一个新的待定常旅客帐户 (3),并从令牌服务请求一个新的一次性令牌 (4)。

When a new frequent flyer registers on the registration page (1), a POST request is sent to the Frequent Flyer endpoint with details about the new member (2). The Frequent Flyer service adds a new pending Frequent Flyer account in the membership database (3) and requests a new single-use token from the token service (4).

该令牌将用于创建一封电子邮件确认消息,发送给新的飞行常客会员。该操作在另一项服务(电子邮件服务)中进行,由另一个团队维护。

The token will be used to create an email confirmation message sent to the new Frequent Flyer member. This happens in a separate service, the email service, which is maintained by a different team.

我们的应用程序使用基于事件的架构来协调和发送这些电子邮件。飞行常客服务发布NewFrequentFlyerEvent事件到消息代理上的专用通道 (5);对此事件感兴趣的其他服务(例如电子邮件服务)可以订阅这些事件。这样,每当有新的飞行常客注册时,电子邮件服务就会准备并发送电子邮件确认消息到相应的地址,其中包含一个包含一次性令牌的链接 (6)。

Our application uses an event-based architecture to coordinate and send these emails. The Frequent Flyer service publishes a NewFrequentFlyerEvent event to a dedicated channel on a message broker (5); other services interested in this event, such as the email service, can subscribe to these events. This way, whenever a new Frequent Flyer registers, the email service will prepare and send an email confirmation message to the appropriate address, with a link containing the single-use token (6).

当新成员收到电子邮件确认消息时,他们点击消息中的链接来确认其电子邮件地址 (7)。这反过来会向电子邮件确认端点发送 POST 请求 (8),从而将成员帐户的状态更新为活动状态。

When the new member receives the email confirmation message, they click on the link in the message to confirm their email address (7). This in turn sends a POST request to the email confirmation endpoint (8), which updates the status of the member’s account to active.

Tracy 现在可以通过登录页面登录她的帐户,该页面使用身份验证服务端点来验证她的电子邮件和密码 (10),然后使用会员服务为她提供帐户概览 (11)。

Tracy can now log on to her account via the login page, which uses the authentication service endpoint to validate her email and password (10), and then the membership service to provide her with an overview of her account (11).

让我们使用此架构来演示一下我们看到的第一个场景的自动化过程:

Let’s walk through the process of automating the first scenario we saw, using this architecture:

  示例:Tracy 注册成为新的常旅客
    鉴于Tracy 没有飞行常客账户
    她注册新的飞行常客账户
时    她确认了她的电子邮件地址
    那么她应该有一个新的标准等级账户,积分为 0
  Example: Tracy registers as a new Frequent Flyer
    Given Tracy does not have a Frequent Flyer account
    When she registers for a new Frequent Flyer account
    And she confirms her email address
    Then she should have a new Standard tier account with 0 points

您可以在 Github 上的示例代码存储库 ( https://github.com/bdd-in-action/second-edition.git ) 中的 chapter-13/api-acceptance-tests 文件夹中看到该测试套件的完整源代码。

You can see the full source code for this test suite in the chapter-13/api-acceptance-tests folder in the sample code repository on Github (https://github.com/bdd-in-action/second-edition.git).

13.4.1 准备测试数据

13.4.1 Preparing the test data

第一步是设置我们未来的常旅客会员 Tracy。一个简单的方法可能是将 Tracy 的详细信息存储在 HOCON 文件中,如下所示:

The first step involves setting up our prospective Frequent Flyer member, Tracy. A simple approach might be to store Tracy’s details in a HOCON file, as shown here:

特蕾西:{
  电子邮件:“tracy@example.org”
  密码:“trac1”
  名字:“Tracy”
  姓氏:“旅行者”
  地址:“10 Pinnack Street, Reading”
  国家:“英国”
  头衔:“夫人”
}
Tracy:  {
  email: "tracy@example.org"
  password: "trac1"
  firstName: "Tracy"
  lastName: "Traveler"
  address: "10 Pinnack Street, Reading"
  country: "United Kingdom"
  title:"Mrs"
}

在我们的 Java 代码中,我们以简单的 Java 记录的形式表示此信息,如下所示:

In our Java code, we represent this information in the form of a simple Java Record, like this one:

公共记录 TravelerRegistration (字符串 firstName, 
                                    字符串姓氏,
                                    字符串标题, 
                                    字符串电子邮件, 
                                    字符串密码,
                                    字符串地址, 
                                    字符串国家){}
public record TravelerRegistration(String firstName, 
                                    String lastName,
                                    String title, 
                                    String email, 
                                    String password,
                                    String address, 
                                    String country) {}

假设我们将旅行者注册详细信息存储在名为 travelers.conf 的文件中。我们可以使用 TypeSafe Config 库 ( https://github.com/lightbend/config ) 从此文件加载这些详细信息,如下所示:

Suppose we are storing our traveler registration details in a file called travelers.conf. We can load these details from this file using the TypeSafe Config library (https://github.com/lightbend/config), like this:

公共类TravelerRegistrationConfig {
 
    公共静态TravelerRegistration forTravelerNamed(String name){
        配置旅行者详情
            = ConfigFactory.load("旅行者").getConfig(名称);
        返回新的TravelerRegistration(
                travelerDetails.getString(“firstName”),
                travelerDetails.getString(“lastName”),
                travelerDetails.getString(“标题”),
                travelerDetails.getString(“电子邮件”),
                travelerDetails.getString(“密码”),
                travelerDetails.getString(“地址”),
                travelerDetails.getString(“国家”)
        (英文):
    }
}
public class TravelerRegistrationConfig {
 
    public static TravelerRegistration forTravelerNamed(String name) {
        Config travelerDetails
            = ConfigFactory.load("travelers").getConfig(name);
        return new TravelerRegistration(
                travelerDetails.getString("firstName"),
                travelerDetails.getString("lastName"),
                travelerDetails.getString("title"),
                travelerDetails.getString("email"),
                travelerDetails.getString("password"),
                travelerDetails.getString("address"),
                travelerDetails.getString("country")
        );
    }
}

这使得我们的第一步定义代码变得简单,只需捕获旅行者的姓名(在本例中为 Tracy)并从 traveler.conf 配置中加载相应的注册详细信息文件:

This makes our first step definition code a simple matter of capturing the name of the traveler (in this case, Tracy) and loading the corresponding registration details from the traveler.conf configuration file:

旅行者注册新会员;
 
@Given("{} 没有飞行常客帐户")
公共 void has_no_frequent_flyer_account(字符串名称){
    新成员 = travelerRegistrationConfig.forTravelerNamed(姓名);
}
TravelerRegistration newMember;
 
@Given("{} does not have a Frequent Flyer account")
public void has_no_frequent_flyer_account(String name) {
    newMember = travelerRegistrationConfig.forTravelerNamed(name);
}

13.4.2 执行 POST 查询:注册飞行常客会员

13.4.2 Performing a POST query: Registering a Frequent Flyer member

下一步是我们首先与其中一个MembershipAPI 端点进行交互。我们需要向 api/frequent-flyer URL 发送一个 POST 查询,并检索服务返回给我们的常旅客号码(我们将需要此号码来完成后续步骤)。在后台,这将在会员数据库中记录一个新的常旅客帐户,并向NewFrequentFlyerEvent消息代理发布。此流程如图 13.4 所示。

The next step is where we first interact with one of the Membership API endpoints. We need to send a POST query to the api/frequent-flyer URL and retrieve the Frequent Flyer number that the service returns to us (we will need this for the steps to follow). Behind the scenes, this will record a new Frequent Flyer account in the membership database and publish a NewFrequentFlyerEvent to the message broker. This flow is illustrated in figure 13.4.

图 13.4 注册常旅客会员

Figure 13.4 Registering a frequent flyer member

避免在步骤定义方法中直接与应用程序层(UI 或 API)交互是一种很好的做法。这就是为什么在前面的章节中,我们了解了如何使用操作类和 Screenplay 任务来创建更易读、更可重用的抽象层,并将其用于步骤定义代码。我们可以对 API 层做类似的事情。

It is good practice to avoid interacting directly with our application layers (UI or APIs) in our step definition methods. This is why, in previous chapters, we saw how to use action classes and Screenplay tasks to create a more readable and more reusable layer of abstraction that we use for our step definition code. We can do something similar with the API layers.

让我们想象一个专门的类,叫做MembershipAPI,它将包含与常旅客会员 API 端点交互的方法。我们的步骤定义类可能看起来像这样:

Let’s imagine a dedicated class, called MembershipAPI, which will contain methods to interact with the Frequent Flyer membership API endpoints. Our step definition class could look something like this:

MembershipAPI membershipAPI = new MembershipAPI();                  
 
@When(“他/她注册了新的飞行常客帐户”)
公共无效注册新频繁飞行账户(){
    newFrequentFlyerNumber = membershipAPI.register(新会员);     
}
MembershipAPI membershipAPI = new MembershipAPI();                 
 
@When("he/she registers for a new Frequent Flyer account")
public void registers_for_a_new_frequent_flyer_account() {
    newFrequentFlyerNumber = membershipAPI.register(newMember);    
}

创建 MembershipAPI 类的新实例。

Create a new instance of the MembershipAPI class.

我们通过调用 MembershipAPI 类的方法与 API 进行交互。

We interact with the API by calling methods of the MembershipAPI class.

方法register()将执行实际的 API 交互;更准确地说,它将执行 POST 请求并从响应中提取常旅客号码。

The register() method will perform the actual API interaction; more precisely, it will perform the POST request and extract the frequent flyer number from the response.

要执行 POST 请求,我们实际上可以使用任何我们喜欢的 REST 客户端库。但是,在编写测试自动化代码时,Rest Assured ( https://rest-assured.io/ ) 是一个不错的选择。正如我们将看到的,Rest Assured 为我们提供了一个可读的 DSL,既可以与 REST API 交互,也可以从响应中提取数据并对其进行断言。

To perform the POST request, we could really use any REST client library we like. However, when writing test automation code, Rest Assured (https://rest-assured.io/) is a great choice. As we will see, Rest Assured gives us a readable DSL to both interact with REST APIs, but also to extract data from, and make assertions about, the responses.

默认情况下,我们的测试应用程序在端口 3000 上运行,因此当我们在本地机器上运行该应用程序时,我们可以访问 http://localhost:3000/api 上的 API 端点。2http://localhost:3000/api/frequent-flyer 提交 POST 请求的 Rest Assured 代码如下所示:

By default, our test application runs on port 3000, so when we run the application on our local machine, we can access the API endpoints on http://localhost:3000/api.2 The Rest Assured code to submit a POST request to http://localhost:3000/api/ frequent-flyer looks something like this:

响应 response = RestAssured.given()                         
        .contentType(ContentType.JSON)                          
        .body(新成员)                                        
        .post(“http://localhost:3000/api/frequent-flyer”);      
Response response = RestAssured.given()                        
        .contentType(ContentType.JSON)                         
        .body(newMember)                                       
        .post("http://localhost:3000/api/frequent-flyer");     

所有 Rest Assured 查询都以对 RestAssured 类的静态调用开始。

All Rest Assured queries start with a static call on the RestAssured class.

我们需要指定将要发布的数据的格式。

We need to specify the format of the data we will be posting.

RestAssured 会自动将我们的 Java 记录转换为 JSON 格式。

RestAssured will automatically convert our Java record into a JSON format.

❹post ()方法提交POST查询,并返回一个Response对象,该对象包含返回的数据和状态码。

The post() method submits the POST query and returns a Response object that contains the returned data and status code.

我们可以通过指定RestAssured.baseURI属性来简化此代码在我们的测试用例开始时,如下所示:

We can simplify this code a little by specifying the RestAssured.baseURI property at the start of our test cases, as shown here:

RestAssured.baseURI =“http://localhost:3000/api”;      
响应 response = RestAssured.given()
        .contentType(内容类型.JSON)
        .body(新成员)
        .post("/飞行常客");
RestAssured.baseURI = "http://localhost:3000/api";     
Response response = RestAssured.given()
        .contentType(ContentType.JSON)
        .body(newMember)
        .post("/frequent-flyer");

定义测试中所有 RestAssured 查询所使用的基本 URL

Defines the base URL to be used for all RestAssured queries in a test

如果我们想要在几个类似的 API 客户端之间重用此配置逻辑,我们可以创建一个像这样的基类,从 serenity.conf 配置文件中读取基本 URL,3如下所示:

If we wanted to reuse this configuration logic across several similar API clients, we might create a base class like this one, that reads the base URL from the serenity.conf configuration file,3 like this one:

公共类ConfigurableAPIClient {
    公共可配置API客户端(){
        RestAssured.baseURI = Serenity.environmentVariables()               
                                      .getProperty("base.url",              
                                           “http://localhost:3000 / api”);    
    }
}
public class ConfigurableAPIClient {
    public ConfigurableAPIClient() {
        RestAssured.baseURI = Serenity.environmentVariables()              
                                      .getProperty("base.url",             
                                           "http://localhost:3000/api");   
    }
}

加载 serenity.conf 配置属性

Loads the serenity.conf configuration properties

读取 base.url 配置属性(如果已定义)

Reads the base.url configuration property if defined

否则使用合理的默认值

Otherwise uses a sensible default value

serenity.conf 文件使用简单的 HOCON 格式,通常包含项目配置详细信息,例如用于 Web 测试的驱动程序和其他报告选项。对于此项目,它仅包含 base.url 属性:

The serenity.conf file uses a simple HOCON format and typically contains project configuration details such as the driver to use for web tests and other reporting options. For this project, it simply contains the base.url property:

base.url =“http://本地主机:3000/api”
base.url = "http://localhost:3000/api"

对象Response允许我们从 API 调用返回的响应中提取信息。创建新的飞行常客帐户时,端点将返回一个完整的 JSON 结构,其中包含有关新飞行常客的详细信息,如下所示:

The Response object allows us to extract information from the response returned to us from the API call. When a new Frequent Flyer account is created, the endpoint will return a complete JSON structure containing details about the new frequent flyer, like this one:

{
    “isActivated”:false,
    “等级”:“标准”,
    “statusPoints”:0,
    “firstName”:“特蕾西”,
    "lastName": "旅行者",
    “title”:“夫人”,
    “电子邮件”:“tracy@example.org”,
    “地址”:“雷丁,皮纳克街 10 号”,
    “国家”:“英国”,
    “常旅客号码”:1000036
}
{
    "isActivated": false,
    "tier": "STANDARD",
    "statusPoints": 0,
    "firstName": "Tracy",
    "lastName": "Traveler",
    "title": "Mrs",
    "email": "tracy@example.org",
    "address": "10 Pinnack Street, Reading",
    "country": "United Kingdom",
    "frequentFlyerNumber": 1000036
}

但是,在本例中,我们只需要常旅客号码。请记住,我们是通过 API 测试功能,而不是测试 API 本身(还会有其他测试),因此从此frequentFlyerNumber响应中提取字段更有意义。将我们自己限制为单个字段可以减少我们对 JSON 响应结构的依赖;例如,如果将另一个字段添加到结构中,我们的测试将不会失败。

However, in this case we only need the frequent flyer number. Remember, we are testing functionality via the API, not testing the API itself (there will be other tests for that), so it makes more sense to simply extract the frequentFlyerNumber field from this response. Limiting ourselves to a single field reduces our dependency on the structure of the JSON response; if another field is added to the structure, for example, our test will not fail.

在 中RestAssured,我们使用一种称为 JSONPath 的符号从 JSON 响应中提取数据。JSONPath 与 XPath 类似,但适用于 JSON 文档。它允许复杂的查询从复杂的 JSON 结构中检索单个值或值集合。

In RestAssured, we use a notation called JSONPath to extract data from a JSON response. JSONPath is similar to XPath, but for JSON documents. It allows sophisticated queries to retrieve individual values or collections of values from complex JSON structures.

在这种情况下,frequentFlyerNumber属性位于 JSON 结构的顶层,因此我们可以使用jsonPath()方法按名称检索它像这样:

In this case, the frequentFlyerNumber attribute is at the top level of the JSON structure, so we can retrieve it by name, using the jsonPath() method like this:

response.jsonPath().getString("frequentFlyerNumber")
response.jsonPath().getString("frequentFlyerNumber")

添加此代码,完成MembershipAPI,包括创建新的飞行常客会员和检索新的飞行常客号码的方法,可能如下所示:

Adding in this code, the complete MembershipAPI class, including the method to create a new Frequent Flyer member and retrieve the new frequent flyer number, might look like this:

公共类 MembershipAPI 扩展了 ConfigurableAPIClient {
 
  公共字符串注册(TravelerRegistration newMember){
      RestAssured.baseURI =“http://localhost:3000/api”;
      响应 response = RestAssured.given()                         
            .contentType(内容类型.JSON)
            .body(新成员)
            .post("/飞行常客");
      返回 response.jsonPath().getString("frequentFlyerNumber");    
  }
}
public class MembershipAPI extends ConfigurableAPIClient {
 
  public String register(TravelerRegistration newMember) {
      RestAssured.baseURI = "http://localhost:3000/api";
      Response response = RestAssured.given()                        
            .contentType(ContentType.JSON)
            .body(newMember)
            .post("/frequent-flyer");
      return response.jsonPath().getString("frequentFlyerNumber");   
  }
}

向 API 端点发送 POST 查询

Sends a POST query to the API endpoint

返回响应对象中的 'frequentFlyerNumber' 字段的值

Returns the value of the 'frequentFlyerNumber' field in the response object

这是一个非常简单的示例,说明如何使用 JSONPath 从 JSON 响应中检索关键数据。在下一节中,我们将学习如何使用 JSONPath 查询更复杂的 JSON结构。

This is a very simple example of how we can use JSONPath to retrieve key data from a JSON response. In the next section, we will learn how to use JSONPath to query more complex JSON structures.

13.4.3 使用 JSONPath 查询 JSON 响应

13.4.3 Querying JSON responses with JSONPath

认为我们正在使用一个 API 调用,该调用返回一些常旅客帐户详细信息以及他们过去的航班列表。确切的 JSON 结构显示在以下清单中。

Suppose we are working with an API call that returns some of the Frequent Flyer account details, along with a list of their past flights. The exact JSON structure is shown in the following listing.

清单 13.1 示例 JSON 响应

Listing 13.1 A sample JSON response

{
  “常旅客号码”:1000036,
  “firstName”:“特蕾西”,
  "lastName": "旅行者",
  “电子邮件”:“tracy@example.org”,
  “航班历史”:[
    {
      “来自”:“伦敦”,
      “到”:“ 巴黎”
      “日期”:“2021-12-15”
    },
    {
      “来自”:“巴黎”,
      “至”:“ 伦敦”
      “日期”:“2021-12-21”
    }
  ],
  “护照”: {
    “号码”:“12345678”,
    “国家”:“英国”
  }
}
{
  "frequentFlyerNumber": 1000036,
  "firstName": "Tracy",
  "lastName": "Traveler",
  "email": "tracy@example.org",
  "flightHistory": [
    {
      "from": "London",
      "to": " Paris",
      "date": "2021-12-15"
    },
    {
      "from": "Paris",
      "to": " London",
      "date": "2021-12-21"
    }
  ],
  "passport": {
    "number": "12345678",
    "country": "United Kingdom"
  }
}

让我们看看如何使用 JSONPath 通过 Rest Assured 查询这个 JSON 文档。

Let’s see how we can use JSONPath to query this JSON document using Rest Assured.

读取顶级字段值

Reading top-level field values

场地JSON 结构顶层的值可以简单地通过名称引用。例如,要读取 Tracy 的姓氏,我们可以使用以下命令:

Field values at the top level of a JSON structure can simply be referred to by name. For example, to read Tracy’s last name, we could use the following:

字符串 lastName = response.jsonPath().getString("lastName");
String lastName = response.jsonPath().getString("lastName");

我们还可以将值转换为其他类型。例如,如果我们需要整数形式的常旅客号码,我们可以使用getInt()方法 反而:

We can also convert values to other types. For example, if we need the frequent flyer number as an integer, we could use the getInt() method instead:

int number = response.jsonPath().getInt("frequentFlyerNumber");
int number = response.jsonPath().getInt("frequentFlyerNumber");

读取嵌套字段值

Reading nested field values

我们可以使用“点”符号访问嵌套字段值。例如,以下代码将读取 Tracy 的护照数字:

We can access nested field values by using the “dot” notation. For example, the following code will read Tracy’s passport number:

int 护照 = response.jsonPath().getInt("护照.号码");
int passport = response.jsonPath().getInt("passport.number");

将结构的各个部分作为对象读取

Reading parts of a structure as an object

有时我们可能希望检索字段集合作为 Java 域对象。例如,假设我们想使用以下 Java 记录来表示 Tracy 的护照详细信息:

Sometimes we might want to retrieve a collection of fields as a Java domain object. For example, suppose we wanted to represent Tracy’s passport details using the following Java record:

公共记录护照(int 号码,String 国家){}
public record Passport(int number, String country) {}

getObject()我们可以通过调用方法直接从 JSON 响应中读取该对象并指定我们想要存储值的类在:

We could read this object directly from the JSON response by calling the getObject() method and specifying the class we want to store the values in:

护照护照 = response.jsonPath()。getObject(“护照”,
                                                  护照.类别);
Passport passport = response.jsonPath().getObject("passport",
                                                  Passport.class);

读取对象集合

Reading collections of objects

JSON路径不仅限于提取单个值或对象。有时我们可能希望读取与给定路径匹配的所有项目。例如,flightHistory.toJSONPath 表达式将匹配航班历史列表中每个航班的目的地字段。我们可以通过使用以下getList()方法获取所有目的地字段, 像这样:

JSONPath is not limited to extracting individual values or objects. Sometimes we might want to read all of the items matching a given path. For example, the flightHistory.to JSONPath expression will match the destination fields of each flight in the flight history list. We could get them all by using the getList() method, like this:

列表 <String> cities = response.jsonPath().getList("flightHistory.to");
List<String> cities = response.jsonPath().getList("flightHistory.to");

我们还可以检索条目列表作为对象列表。例如,假设我们有一个简单的 Java 记录来表示航班历史记录,如下所示:

We can also retrieve a list of entries as a list of objects. For example, suppose we had a simple Java record to represent the flight history records, like this one:

公共记录航班(字符串从,字符串到,字符串日期){}
public record Flight(String from, String to, String date) {}

Flight我们可以通过指定记录类从 JSON 响应中加载记录列表:

We could load a list of Flight records from the JSON response simply by specifying the record class:

列出<Flight>航班 
               = response.jsonPath().getList("flightHistory",Flight.class);
List<Flight> flights 
               = response.jsonPath().getList("flightHistory",Flight.class);

JSONPath 是一个功能强大的工具,它允许我们提取特定场景所需的信息。通过只获取我们真正需要的信息,并忽略偶然字段,我们可以帮助提高测试自动化代码的灵活性和稳定性。我们将在后面看到它的更多用途章。

JSONPath is a powerful tool, which allows us to extract just the information we need for a particular scenario. By only getting the information we really need, and ignoring incidental fields, we can help make our test automation code more flexible and robust. We will see more of its use later in this chapter.

13.4.4 执行 GET 查询:确认常旅客地址

13.4.4 Performing a GET query: Confirming the frequent flyer address

在我们的场景中,下一步是 Tracy 确认她的电子邮件地址。图 13.5 更详细地描述了此过程。

The next step in our scenario is where Tracy confirms her email address. Figure 13.5 describes this process in more detail.

图13.5 确认邮箱地址

Figure 13.5 Confirming the email address

当电子邮件服务NewFrequentFlyerEvent从消息代理收到 时,该过程开始 (1)。电子邮件服务从令牌服务中获取一次性令牌 (2),并向 Tracy 发送一封电子邮件,其中包含一个链接,她可以通过该链接确认她的电子邮件地址 (3)。当 Tracy 点击链接 (4) 时,电子邮件确认页面会向会员 API 发送一个 POST 请求,其中包含一条消息,其中包含 Tracy 的电子邮件和常旅客号码以及一次性令牌 (5)。

The process starts when the email service receives a NewFrequentFlyerEvent from the message broker (1). The email service fetches a single use token from the token service (2) and sends an email to Tracy containing a link where she can confirm her email address (3). When Tracy clicks on the link (4) the email confirmation page sends a POST request to the Membership API with a message containing Tracy’s email and frequent flyer number along with the single-use token (5).

对于我们的测试场景,我们只需要关注最后一步。电子邮件的生成和发送由电子邮件服务完成,该服务由一个单独的团队开发。像这样的解耦架构的优点之一是,它使工作更容易独立和并行地完成。我们不需要有一个可运行的电子邮件服务来实现电子邮件确认逻辑;我们所需要的只是就一次性令牌服务的工作方式达成一致。

For our test scenario we only need to focus on this last step. Generating and sending the email is done by the email service, which is being developed by a separate team. One of the advantages of decoupled architectures like this one is that it makes it much easier for work to be done independently and in parallel. We do not need to have a working email service to be able to implement the email confirmation logic; all we need is to agree on how the single use token service will work.

具体来说,第三步(“当她确认她的电子邮件地址时”)需要做两件事:

In concrete terms, the third step (“when she confirms her email address”) needs to do two things:

  1. 从 Token API 获取一次性令牌。

  2. Fetch the single-use token from the Token API.

  3. 使用会员 API 确认电子邮件地址。

  4. Confirm the email address using the Membership API.

这两个步骤都与不同的端点交互。我们可以将所有这些逻辑包含在同一个类中,但将 Token API 逻辑与会员 API 代码分开会更简洁、更易于维护。为了与 Token API 交互,我们将创建一个新TokenAPI

Both of these steps interacts with a different endpoint. We could include all of this logic in the same class, but it would be cleaner and more maintainable to keep the Token API logic separate from the membership API code. To interact with the Token API, we will create a new TokenAPI class.

使用此类,步骤定义代码将如下所示:

Using this class, the step definition code would look like this:

TokenAPI tokenAPI = new TokenAPI();
 
@When(“他/她确认她的电子邮件地址”)
公共无效确认电子邮件地址(){
    emailToken = tokenAPI.getEmailToken(newFrequentFlyerNumber);
    membershipAPI.confirmEmail(新飞行常客号码, 
                               新会员.email(), 
                               电子邮件令牌);
}
TokenAPI tokenAPI = new TokenAPI();
 
@When("he/she confirms her email address")
public void confirms_email_address() {
    emailToken = tokenAPI.getEmailToken(newFrequentFlyerNumber);
    membershipAPI.confirmEmail(newFrequentFlyerNumber, 
                               newMember.email(), 
                               emailToken);
}

获取一次性令牌

Fetching the single-use token

我们需要实现的第一个方法是getEmailToken(),使用 GET 请求检索一次性令牌:

The first method we need to implement is getEmailToken(), which retrieves the single-use token using a GET request:

公共类TokenAPI扩展了ConfigurableAPIClient {
 
    公共字符串 getEmailToken(字符串 commonFlyerNumber){
        返回 RestAssured.given()                    
                          .pathParam("id", 常旅客号码)      
                          .get(“/tokens/frequent-flyer/{id}”)        
                          .getBody().asString();                     
    }
}
public class TokenAPI extends ConfigurableAPIClient {
 
    public String getEmailToken(String frequentFlyerNumber) {
        return RestAssured.given()                    
                          .pathParam("id", frequentFlyerNumber)     
                          .get("/tokens/frequent-flyer/{id}")       
                          .getBody().asString();                    
    }
}

定义一个“id”变量,我们可以在路径表达式中使用

Defines an "id" variable that we can use in the path expression

执行 GET 操作,将常旅客号码注入 {id} 占位符中

Performs a GET operation, injecting the frequent flyer number into the {id} placeholder

以字符串形式检索返回值

Retrieves the returned value as a string

许多 GET 操作会返回更复杂的 JSON 结构,我们需要处理这些结构或将其转换为 Java 类或记录。但由于此 API 返回的是一个简单的字符串表达式,因此我们可以直接返回并在我们的代码。

Many GET operations return more complex JSON structures, which we need to process or convert to a Java class or record. But since this API returns a simple string expression, we can simply return and use this value directly in our code.

确认电子邮件地址

Confirming the email address

最后一步是确认电子邮件地址。为此,我们需要发送一个简单的 JSON 结构,其中包含三个字段:姓名、常旅客号码和电子邮件地址。最简单的方法之一是使用Java记录:

The final step is to confirm the email address. To do this, we need to send a simple JSON structure containing three fields: name, frequent flyer number, and email address. One of the simplest way to do this would be to use a Java record:

公共记录EmailValidation(String oftenFlyerNumber,
                              字符串电子邮件,
                              字符串标记){}
 
ConfirmEmail() 方法与 Register() 方法类似,其中 
我们将一个 Java 对象传递到请求主体中:
 
公共无效确认电子邮件(字符串频繁飞行者号码, 
                         字符串电子邮件, 
                         字符串标记){
    RestAssured.given()
        .contentType(内容类型.JSON)
        .body(新EmailValidation(frequentFlyerNumber,电子邮件,令牌))    
        .post(“/frequent-flyer/email-confirmation”)                      
        .then().statusCode(201);                                         
}
public record EmailValidation(String frequentFlyerNumber,
                              String email,
                              String token){}
 
The confirmEmail() method will be similar to the register() method, where 
 we pass a Java object into the body of the request:
 
public void confirmEmail(String frequentFlyerNumber, 
                         String email, 
                         String token) {
    RestAssured.given()
        .contentType(ContentType.JSON)
        .body(new EmailValidation(frequentFlyerNumber, email, token))   
        .post("/frequent-flyer/email-confirmation")                     
        .then().statusCode(201);                                        
}

该对象将被转换为JSON文档并在请求体中提交。

This object will be converted to a JSON document and submitted in the request body.

提交 POST 请求

Submits a POST request

检查响应的状态码,如果不是预期值,则抛出异常

Checks the status code of the response and throws an exception if it is not the expected value

使用 then() 方法检查结果

Checking outcomes with the then() method

方法then()最后一行是检查响应并确保操作成功的便捷方法。此方法允许我们使用简单值和更复杂的 Hamcrest 匹配器检查响应的状态和内容。例如,以下代码检查状态代码的值为 201、字段frequentFlyerNumber是否等于我们传入的字段,以及字段token是否不为空:

The then() method in the last line is a convenient way to check the response and to make sure that the operation was successful. This method lets us check the status and content of the response using both simple values and more sophisticated Hamcrest matchers. For example, the following code checks that the status code has a value of 201, the frequentFlyerNumber field is equal to the one we passed in, and that the token field is not empty:

RestAssured.given()
        .contentType(内容类型.JSON)
        .body(新 EmailValidation(frequentFlyerNumber、电子邮件、令牌))
        .post(“/常旅客/电子邮件确认”)
        。然后()
        .状态码(201)
        .body("frequentFlyerNumber", equalTo(frequentFlyerNumber))
        .body("token", 不是(emptyOrNullString()));
RestAssured.given()
        .contentType(ContentType.JSON)
        .body(new EmailValidation(frequentFlyerNumber, email, token))
        .post("/frequent-flyer/email-confirmation")
        .then()
        .statusCode(201)
        .body("frequentFlyerNumber", equalTo(frequentFlyerNumber))
        .body("token", not(emptyOrNullString()));

我们还可以使用 JSONPath 表达式对对象集合做出断言。例如,以下代码将检查航班历史记录中的目的地列表:

We can also make assertions about collections of objects, using JSONPath expressions. For example, the following code will check the list of destinations in the flight history:

RestAssured.when()
           .get("/frequent-flyer/{id}/history", id)
           。然后()
           .body("flightHistory.to", hasItems("巴黎", "伦敦"));
RestAssured.when()
           .get("/frequent-flyer/{id}/history", id)
           .then()
           .body("flightHistory.to", hasItems("Paris", "London"));

13.4.5 部分 JSON 响应:检查新的飞行常客帐户详细信息

13.4.5 Partial JSON Responses: Checking the new Frequent Flyer account details

该场景的最后一步是检查新的飞行常客帐户是否已成功创建并激活:

The last step of the scenario involves checking that the new Frequent Flyer account has been successfully created and activated:

那么她应该有一个新的标准等级账户,积分为 0
Then she should have a new Standard tier account with 0 points

/api/ frequent-flyer/{id}我们可以通过向端点发送 GET 查询来读取当前的会员详细信息,并注明会员的常旅客号码(见图13.6)。

We can read the current membership details by sending a GET query to the /api/ frequent-flyer/{id} endpoint, and specifying the member’s frequent flyer number (see figure 13.6).

图 13.6 检索会员详细信息

Figure 13.6 Retrieving membership details

此查询将返回如下所示的 JSON 文档,其中包含有关飞行常客的各种详细信息:

This query will return a JSON document like the following, containing various details about the Frequent Flyer:

{
  “isActivated”:true,
  “等级”:“标准”,
  “statusPoints”:0,
  “firstName”:“特蕾西”,
  "lastName": "旅行者",
  “title”:“夫人”,
  “电子邮件”:“tracy@example.org”,
  “地址”:“雷丁,皮纳克街 10 号”,
  “国家”:“英国”,
  “常旅客号码”:1000018
}
{
  "isActivated": true,
  "tier": "STANDARD",
  "statusPoints": 0,
  "firstName": "Tracy",
  "lastName": "Traveler",
  "title": "Mrs",
  "email": "tracy@example.org",
  "address": "10 Pinnack Street, Reading",
  "country": "United Kingdon",
  "frequentFlyerNumber": 1000018
}

随着应用程序的增长,此数据结构可能会增长,需要更多字段来满足其他要求。但是,出于测试目的,我们实际上只需要三个字段:字段isActivated(用于了解激活状态)、积分数量和会员等级。

As the application grows, this data structure might grow, with additional fields needed for other requirements. However, for the purposes of our test, we really need only three fields: the isActivated field (to know the activation status), the number of points, and the membership tier.

我们可以使用 Java 记录从响应对象中提取这些数据:

We can extract this data from the response object by using a Java record:

公共记录 TravelerAccountStatus(int statusPoints,
                                     MembershipTier 等级,
                                     布尔值是否已激活){}
public record TravelerAccountStatus(int statusPoints,
                                     MembershipTier tier,
                                     boolean isActivated) {}

但是,有一个问题。默认情况下,Rest Assured 将尝试为 JSON 文档中的每个字段查找属性,如果找不到,则会抛出错误。为了解决此限制,我们可以使用@JsonIgnoreProperties属性, 像这样:

However, there is a catch. By default, Rest Assured will try to find a property for every field in the JSON document and throw an error when it doesn’t find them. To work around this constraint, we can use the @JsonIgnoreProperties attribute, like this:

@JsonIgnoreProperties(ignoreUnknown = true)
公共记录 AccountStatus(int statusPoints,
                            MembershipTier 等级,
                            布尔值是否已激活){}
@JsonIgnoreProperties(ignoreUnknown = true)
public record AccountStatus(int statusPoints,
                            MembershipTier tier,
                            boolean isActivated) {}

我们现在可以向相应的端点提交 GET 请求,检索完整的帐户状态 JSON 数据结构,并将结果转换为AccountStatus对象仅包含我们感兴趣的字段:

We can now submit a GET request to the corresponding endpoint, retrieve the full account status JSON data structure, and convert the results to an AccountStatus object containing only the fields we are interested in:

公共 AccountStatus getMemberStatus(String oftenFlyerNumber){
    返回 RestAssured.given()
                    .get("/frequent-flyer/{number}", oftenFlyerNumber)    
                    .jsonPath()                
                    .getObject(“.”, AccountStatus.class);                    
}
public AccountStatus getMemberStatus(String frequentFlyerNumber) {
    return RestAssured.given()
                    .get("/frequent-flyer/{number}", frequentFlyerNumber)   
                    .jsonPath()                
                    .getObject(".", AccountStatus.class);                   
}

使用路径中的常旅客号码执行 GET 操作

Performs a GET operation using the frequent flyer number in the path

将 JSON 文档转换为 AccountStatus 对象

Converts the JSON document to an AccountStatus object

在步骤定义方法中,我们现在可以使用该getMemberStatus()方法检索这些数据并将其值与预期值进行比较:

In the step definition method, we can now use the getMemberStatus() method to retrieve this data and compare their values with the expected ones:

@Then(“他/她应该有一个新的{tier}等级帐户,其中包含{int}积分”)
public void should_have_a_new_account_with_points(会员等级, 
                                                  整数点){
    旅行者账户状态 accountStatus 
        =会员API.获取会员状态(新常飞旅客编号);
 
    断言(accountStatus.statusPoints())。是否等于(积分);
    断言(accountStatus.tier())。是否等于(tier);
}
@Then("he/she should have a new {tier} tier account with {int} points")
public void should_have_a_new_account_with_points(MembershipTier tier, 
                                                  Integer points) {
    TravelerAccountStatus accountStatus 
        = membershipAPI.getMemberStatus(newFrequentFlyerNumber);
 
    assertThat(accountStatus.statusPoints()).isEqualTo(points);
    assertThat(accountStatus.tier()).isEqualTo(tier);
}

至此,场景的步骤定义已完成,因此该场景现在应该可以运行。但是,我们还需要做一件事:场景完成后,我们需要删除我们创建的常旅客会员,并将系统恢复到初始状态状态。

This completes the step definitions for the scenario, so this scenario should now run. However, there is one more thing we need to do: once the scenario has finished, we will need to delete the Frequent Flyer member we have created and restore the system to its initial state.

13.4.6 执行 DELETE 查询:事后清理

13.4.6 Performing a DELETE query: Cleaning up afterward

典型的 REST API,我们可以使用 HTTP DELETE 命令从系统中删除记录。我们的会员 API 服务也不例外:我们可以通过向/api/frequent-flyer/{id}端点发送 DELETE 请求来删除常旅客帐户

In a typical REST API, we can use the HTTP DELETE command to delete a record from the system. Our membership API service is no exception: we can delete a Frequent Flyer account by sending a DELETE request to the /api/frequent-flyer/{id} endpoint.

不过,我们不想在每个场景的最后一步都这样做;这样很难维护。一种更简单的方法是使用 Cucumber@After注释。带有 注释的方法@After(如这里所示)将在每个场景结束时执行。让我们实现一个名为deleteFrequentFlyer()

We don’t want to have to do this in the last step of every scenario, though; that would be hard to maintain. A simpler approach is to use the Cucumber @After annotation. Methods annotated with @After, like the one shown here, will be executed at the end of each scenario. Let’s implement a method called deleteFrequentFlyer():

@后
公共无效删除FrequentFlyerAccount(){
    会员API.删除常旅客(新常旅客号码);
}
实现起来很简单:
 
公共无效删除FrequentFlyer(字符串frequentFlyerNumber){
    RestAssured.when()
               .delete("/frequent-flyer/{id}", oftenFlyerNumber)      
               .then().statusCode(200);                                  
}
@After
public void deleteFrequentFlyerAccount() {
    membershipAPI.deleteFrequentFlyer(newFrequentFlyerNumber);
}
The implementation is simple:
 
public void deleteFrequentFlyer(String frequentFlyerNumber) {
    RestAssured.when()
               .delete("/frequent-flyer/{id}", frequentFlyerNumber)     
               .then().statusCode(200);                                 
}

使用路径中的常旅客号码执行删除操作

Performs a DELETE operation using the frequent flyer number in the path

确保操作成功

Makes sure the operation succeeded

使用@After注释以及 REST 端点通常是场景结束后清理测试数据的快速有效方法终止。

Using the @After annotation along with REST endpoints is often a fast and effective way to clean up test data after a scenario has terminated.

13.5 自动化更细粒度的场景并与外部服务交互

13.5 Automating more granular scenarios and interacting with external services

到目前为止,我们看到的场景是高级用户旅程场景,它描绘了完整的注册流程,但这些旅程场景只占我们验收标准的很小一部分。我们的验收标准的大部分将采用更小、更细粒度、更灵活的场景形式,探索用户旅程中的具体业务规则。

The scenario we have seen so far is a high-level user-journey scenario that maps out the full registration process, but these journey scenarios should only make a very small proportion of our acceptance criteria. The bulk of our acceptance criteria will take the form of smaller, more granular, and more nimble scenarios that explore specific business rules within a user journey.

我们编写的第二个场景就是这些更细粒度的示例之一的一个很好的例子:

The second scenario we wrote is a good example of one of these more granular examples:

  例如:Tracy 的帐户最初处于待激活状态
    鉴于Tracy 没有飞行常客账户
    她注册新的飞行常客账户
时    然后她应该收到一封带有电子邮件验证链接的电子邮件
    她的帐户应等待激活
  Example: Tracy's account is initially pending activated
    Given Tracy does not have a Frequent Flyer account
    When she registers for a new Frequent Flyer account
    Then she should be sent an email with an email validation link
    And her account should be pending activation

当我们自动化此类场景时,我们通常可以使用与上一节中看到的非常相似的技术,而且很多时候,我们可以重用为前面的步骤编写的 API 客户端代码。在这种情况下,前两个步骤与我们之前的场景相同,我们只需要关注第三和第四个步骤。

When we automate scenarios like this one, we can generally use very similar techniques to those we saw in the previous section, and very often, we can reuse the API client code we wrote for previous steps. In this case, the first two steps are identical to our previous scenario, and we only need to focus on the third and fourth ones.

第三步很有趣,因为我们不能简单地调用我们的服务端点之一。在这一步中,我们需要检查电子邮件验证消息是否已发送给新的常旅客。

The third step is interesting, because we cannot simply call one of our service endpoints. In this step, we need to check that the email validation message was sent to the new frequent flyer.

我们可以通过几种方式执行此检查。我们可以设置一个电子邮件服务器并监控我们的常旅客的收件箱。4某些情况下,由于其重要性或风险,这可能使其成为一种有用的策略。但是,在其他情况下,这些服务可能由其他团队或部门实施和管理。如果我们使用事件驱动架构,就像常旅客应用程序的情况一样,那么只需监听相应的事件就足够了,而无需检查实际的电子邮件传递情况。

There are a few ways we could perform this check. We could set up an email server and monitor the inbox of our frequent flyer.4 And there are some scenarios where the importance or risks involved may make this a useful strategy. However, in other cases these services may be implemented and managed by other teams or departments. If we are using an event-driven architecture, as is the case for the Frequent Flyer application, it may be enough to simply listen for the appropriate event, without needing to check the actual email delivery.

在这两种情况下,步骤定义方法中的自动化代码都应尝试表达步骤的意图,而不是实现。例如,我们可以创建一个EmailMonitor封装这个逻辑并将细节隐藏在一个适当命名的方法中:

In both cases, the automation code in the step definition method should try to express the intent of the step rather than the implementation. For example, we could create an EmailMonitor class to encapsulate this logic and hide the details in an appropriately named method:

@步骤
电子邮件监控电子邮件;
 
@Then(“应该向他/她发送一封带有电子邮件验证链接的电子邮件”)
公共 void shouldBeSentAnEmailWithAnEmailValidationLink() {
    断言(
      电子邮件.newAccountConfirmationMessageSentTo(newMember.email()))
    是真();
}
@Steps
EmailMonitor emails;
 
@Then("he/she should be sent an email with an email validation link")
public void shouldBeSentAnEmailWithAnEmailValidationLink() {
    assertThat(
      emails.newAccountConfirmationMessageSentTo(newMember.email()))
    .isTrue();
}

班级EmailMonitor然后可以自由选择是否查询事件总线日志、订阅特定消息类型或使用外部服务来监控发送的电子邮件消息出去。

The EmailMonitor class is then free to choose whether to query the event bus logs, subscribe to a particular message type, or use an external service to monitor the email messages that go out.

13.6 测试 API 或使用 API 进行测试

13.6 Testing the APIs or testing with the APIs

到目前为止,我们看到的场景旨在通过 API 定义和探索业务规则和行为。它们并非旨在测试 API 本身。使用单元或集成测试工具以及与开发相同的技术堆栈,通常更容易测试 API 设计的细节,例如包含哪些字段以及返回哪些错误代码本身。

The scenarios we have seen so far are designed to define and explore the business rules and behavior, through the APIs. They are not designed to test the APIs themselves. Testing the details of your API design, such as what fields are included and what error codes are returned, are often easier to do using unit or integration testing tools and with the same technology stack as the development itself.

概括

Summary

  • 我们可以描述和测试涉及与微服务和其他 API 层交互的业务需求。

  • We can describe and test business requirements that involve interacting with microservices and other API layers.

  • 与用户界面交互相比,使用 API 调用可以更快、更可靠地测试许多业务需求。即使是用户旅程场景也常常可以通过 API 交互以独特的方式实现自动化。

  • We can test many business requirements more quickly and more reliably using API calls than by interacting with the user interface. Even user-journey scenarios can often be automated uniquely via API interactions.

  • Rest Assured 是一个开源库,我们可以使用它来执行各种 REST API 调用作为自动验收测试的一部分。

  • Rest Assured is an open source library that we can use to perform various REST API calls as part of our automated acceptance tests.

  • JSONPath 是一种查询语言,我们可以使用它来查询从 API 调用收到的响应并检查它们是否包含预期结果。

  • JSONPath is a query language we can use to query the responses we receive from API calls and check that they contain the expected results.

  • 实践 BDD 的团队还可以使用 BDD 技术(例如跨团队 API 发现会话)来探索每个 API 应该提供哪些服务。

  • Teams practicing BDD can also use BDD techniques such as cross-team API discovery sessions to explore exactly what services each API should provide.

在下一章中,我们将不再使用 Java,而是了解如何使用以下工具来实现 BDD 实践和测试自动化技术:JavaScript。

In the next chapter, we move away from Java and see how we can implement BDD practices and test automation techniques for projects using JavaScript.


1  有关微服务架构的更深入讨论,请参阅José Peralta 的《微服务 API》(Manning,2022 年)和Chris Richardson 的《微服务模式》(Manning,2018 年)。

1  For a more in-depth treatment of microservice architectures, see Microservice APIs by José Peralta (Manning, 2022) and Microservice Patterns by Chris Richardson (Manning, 2018).

2  在实际应用中,以及 Github 上的示例项目中,此地址是可配置的,以便能够与不同的环境进行交互。但为了简单起见,本章的代码示例中不会显示这一点。

2  In a real application, and in the sample project on Github, this address is configurable to be able to interact with different environments. However, for simplicity, this will not be shown in the code samples in this chapter.

3  在 Serenity BDD 项目中,测试配置数据存储在名为 serenity.conf 的文件中,您可以在 src/test/resources 目录中找到该文件。

3  In a Serenity BDD project, test configuration data is stored in a file called serenity.conf, which you will find in the src/test/resources directory.

4  甚至还有一些公司,例如 Mailinator ( https://www.mailinator.com/ ) 可以为您提供测试基础设施,您可以在其中以真实的方式测试电子邮件和短信。

4  There are even companies, such as Mailinator (https://www.mailinator.com/) that can provide you with a test infrastructure where you can test email and SMS messages in a realistic way.

14 使用 Serenity/JS 为现有系统提供可执行规范

14 Executable specifications for existing systems with Serenity/JS

本章封面

This chapter covers

  • 使用旅程地图来识别和可视化高价值工作流程和测试场景
  • Using Journey Mapping to identify and visualize high-value workflows and test scenarios
  • 使用分层架构和 Serenity/JS 设计可扩展的测试自动化系统
  • Using layered architecture and Serenity/JS to design scalable test automation systems

到目前为止,您已经了解了如何使用自动验收测试作为设计工具,帮助您的团队逐步开发软件系统,同时您也逐渐了解了系统需要实施的规则以及系统需要支持的业务场景。但是,您可能并不总是有机会从头开始一个全新的项目,您可能会发现自己处于需要对现有或部分现有系统进行验收测试改造,然后才能使用它们来指导任何新功能的开发的情况。

So far, you’ve learned how to use automated acceptance tests as a design tool that helps your team evolve a software system gradually, together with your growing understanding of the rules the system needs to implement and the business scenarios it needs to support. However, you might not always have the luxury of starting fresh on a greenfield project, and you could find yourself in a situation where you need to retrofit acceptance tests to an existing or partially existing system before you can use them to guide development of any new features.

为了帮助您做好准备,在本章中,我们将假设 Flying High Airlines 系统的大部分内容已经构建完成,但没有任何验收测试覆盖。通常情况下,我们的首要任务是围绕系统的关键部分创建一个自动化测试安全网,以使开发团队能够引入改进并降低此类改进导致回归的风险,从而对现有功能产生负面影响。

To help you prepare for that, in this chapter we’ll pretend that most of the Flying High Airlines’ system has already been built without any acceptance test coverage. As is typically the case, our first task is to create a safety net of automated tests around the critical parts of the system to enable the development team to introduce improvements and reduce the risk of such improvements causing regressions, which would negatively affect the existing functionality.

虽然尝试通过编写或生成尽可能多的测试脚本来完成任务可能很诱人,但还有一种更有效的方法来实现这一点。在本章中,您将学习旅程地图——一种强大的促进技术,用于识别高价值工作流并可视化高优先级测试场景。然后,我们将研究使用分层架构来设计可扩展的测试自动化系统,并开始探索 Serenity/JS 测试自动化框架,以便为您概述 Java 和基于 JavaScript 的工具,以支持您的团队采用 BDD。(您可以在https://serenity-js.org上了解有关 Serenity/JS 的更多信息,并在https://github.com/serenity-js上了解有关示例和参考实现的更多信息。)

While it might be tempting to try to fulfill our task by writing or generating as many test scripts as possible, there is a far more effective way to go about this. In this chapter, you’ll learn Journey Mapping—a powerful facilitation technique to identify high-value workflows and visualize high-priority test scenarios. Then, we’ll investigate using layered architecture to design scalable test automation systems and begin to explore the Serenity/JS test automation framework to give you an overview of both Java and JavaScript-based tools to support BDD adoption on your team. (You can learn more about Serenity/JS at https://serenity-js.org and about examples and reference implementations at https://github.com/serenity-js.)

14.1 使用旅程地图探索未知领域

14.1 Navigating an uncharted territory with Journey Mapping

自动化对现有软件系统进行测试通常感觉就像在未知领域中徘徊;您可能会走一条比必要更长的路线,错过捷径,被意想不到的障碍物绊倒,或者在试图找到方向时绕圈子。实际上,这通常意味着将自动化限制在针对完全组装和部署的系统运行的端到端测试,错过将测试与编程 API 集成的机会,没有足够关注系统运行的环境,或者没有考虑业务优先级和风险。

Automating tests of an existing software system often feels like wandering into an uncharted territory; you might take a route that’s much longer than necessary, miss a shortcut, trip over an unexpected obstacle, or walk in circles as you’re trying to get your bearings. In practical terms, this typically means limiting automation to end-to-end tests running against a fully assembled and deployed system, missing opportunities to integrate tests with programmatic APIs, not paying enough attention to the context in which the system operates, or not taking the business priorities and risks into consideration.

尽管您到目前为止所学的 BDD 技术通常用于指导新特性和新功能的开发,但您也可以使用它们来指导现有系统的自动化测试的开发。在本节中,我们将向您展示如何使用一种称为旅程图的技术来识别我们的系统需要支持的关键场景,并帮助我们相应地确定测试自动化工作的优先级。在第 15 章中,您将学习如何在代码中对这些场景进行建模以创建自动化测试。

Even though the BDD techniques you’ve learned about so far are typically used to guide development of new features and new functionality, you can use them to guide the development of automated tests for existing systems as well. In this section, we’ll show you how to use a technique called Journey Mapping to identify the critical scenarios that our system needs to support and to help us prioritize our test automation work accordingly. In chapter 15, you’ll learn how to model those scenarios in code to create automated tests.

由于我们正在使用已经构建好的系统,因此如图 14.1 所示,总体方法是将我们的重点从发现应该提供哪些功能转移到发现如何使用现有功能以及哪些功能是参与者实现其目标的关键路径。

Since we’re working with a system that’s already been built, the overall approach, as shown in figure 14.1, is to shift our focus from discovering what features should be delivered to discovering how the existing features are used and which ones are on the critical path to actors achieving their goals.

图 14.1 为现有系统创建旅程地图的总体方法要求我们将重点从发现需要构建哪些功能转移到发现如何使用现有功能。

Figure 14.1 The overall approach to creating Journey Maps for existing systems requires us to shift focus from discovering what features need to be built to discovering how the existing features are used.

14.1.1 确定参与者和目标以了解业务环境

14.1.1 Determine actors and goals to understand the business context

编写现有系统的任何自动化测试时,不仅要从技术角度理解系统,更重要的是从业务角度理解系统。这意味着您需要了解系统运行的业务环境 — 不仅要了解系统是如何构建的,还要了解系统是什么以及是为谁构建的。

Before writing any automated tests of an existing system, it’s crucial to understand the system not just from the technical perspective but, more importantly, from the business perspective. This means you need to gain an understanding of the business context in which the system operates—not only how the system is built, but also what the system is and who it’s built for.

充分了解创建系统的动机、系统预期支持的最重要的工作流程以及预期服务的受众,对于帮助您更有效地安排自动化工作的优先级并专注于最重要的工作流程至关重要。

Having a good understanding of the reasons that motivated the creation of the system, the most important workflows the system is expected to support and the audiences it is expected to serve, is critical to helping you prioritize your automation work more effectively and focus on the workflows that matter the most.

要做到这一点,您需要做的第一件事就是探索促使创建系统的业务需求,即系统努力解决的业务问题。为了帮助您专注于实现具有最高业务价值的测试场景,您还需要很好地了解系统支持的关键工作流程以及依赖它们按预期工作的受众。例如,在线商店的业务目的可能是向您的客户销售产品,以便他们不必访问实体店。这里最重要的工作流程可能是让客户找到并购买他们感兴趣的产品,并要求退款或更换不符合他们期望的产品。虽然典型的在线商店很可能有很多额外的功能,比如星级评定、评论、推荐等等,但如果找到合适的产品并进行购买的核心功能不起作用,整个系统可能就不复存在了。

To do that, the first thing you need to do is to explore the business need that motivated the creation of the system, the business problem the system strives to address. To help you focus on implementing test scenarios that have the highest business value, you’ll also need a good understanding of the critical workflows the system enables and the audiences who rely on them to work as expected. For example, the business purpose of an online store could be to sell products to your customers so that they don’t have to visit the physical locations. The most important workflows here could be for the customers to find and purchase the products they’re interested in and request a refund or replacement of products that did not meet their expectations. While a typical online store will most likely have a lot of additional functionality, such as star ratings, reviews, recommendations, and so on, if the core functionality of finding the right product and making the purchase doesn’t work, the whole system might as well cease to exist.

初学者的心态

Beginner’s mind

乍一看,向业务发起人询问系统的目的似乎是徒劳的。毕竟,系统存在的原因难道不应该很明显吗?我们不是他们雇来做这项工作的专家吗?难道我们不应该知道吗?

At first glance, asking the business sponsors about the purpose of a system might seem like an exercise in futility. After all, shouldn't it be obvious why a system exists? Aren’t we the experts they’ve hired to do the job? Shouldn’t we just know?

即使我们过去曾经研究过类似类型的系统,也需要以初学者的心态来对待我们的任务,放弃我们对系统应该做什么和不应该做什么的假设、期望和先入为主的观念。

Even if we have worked on similar types of systems in the past, it’s important to approach our task with a beginner’s mind, dropping our assumptions, expectations, and preconceived ideas about what the system should and shouldn’t do in our opinion.

询问每个利益相关者的观点将有助于我们更好地了解系统的业务背景,并尽早发现任何不一致和矛盾。这也将使我们有机会以客观和无可指责的方式捕捉和可视化它们,并尽早建立对系统及其目标的共同理解。

Asking each stakeholder about their perspective will help us understand the business context of the system better and highlight any inconsistencies and contradictions sooner. It will also give us an opportunity to capture and visualize them in an objective and blameless way and build a shared understanding of the system and its goals sooner.

Flying High Airlines 应用程序的业务目的是什么?代表 Flying High Airlines 的业务赞助商可能会说,航班预订系统的主要目的是让旅行者预订航班。这有助于我们确定第一个参与者,即旅行者,以及他们的目标,即预订航班,我们可以在旅程地图上捕捉到这些目标,如图 14.2 所示。

What is the business purpose of the Flying High Airlines app? A business sponsor representing the Flying High Airlines might say that the primary purpose of the flight booking system is to enable travelers to book a flight. This helps us to identify the first actor, the traveler, and their goal, to book a flight, which we can capture on a Journey Map, as shown in figure 14.2.

图 14.2 旅程地图上的参与者及其目标。随着您对被测系统的理解不断加深,您可以添加其他参与者和目标。

Figure 14.2 Actors and their goals on a Journey Map. You can add additional actors and goals as your understanding of the system under test expands.

当然,大多数软件系统将支持多个类别的参与者,每个参与者都有多个目标,随着您对系统的理解不断扩展,您可能会发现将其他参与者添加到您的旅程地图中很有用。为了简单起见,我们将旅程地图限制为一个类别的参与者,即旅行者,以及预订一个单一的目标航班。

Of course, most software systems will support multiple categories of actors, each with multiple goals, and you might find it useful to add other actors to your Journey Map as your understanding of the system expands. To keep things simple for now, we’ll limit our Journey Map to just one category of actors, the travelers, and a single goal to book a flight.

14.1.2 确定哪些工作流程支持感兴趣的目标

14.1.2 Determine what workflows support the goals of interest

现在在您理解系统存在的原因以及其主要受众是谁之后,您需要进一步了解此类受众的代表将如何实现预订航班的目标。为此,您需要从高层次上了解此类参与者为实现其目标需要完成的任何工作流程。例如,您可能会了解到,要预订航班,旅行者需要找到感兴趣的航班,然后进行预订,如图 14.3 所示。

Now that you understand why the system exists and who its primary audience is, you need to understand more about how a representative of such audience would go about accomplishing their goal of booking a flight. To do that, you need a high-level view of any workflows such actor would need to complete to accomplish their goal. For example, you might learn that to book a flight a traveler needs to find a flight of interest and then make a booking, as shown in figure 14.3.

图 14.3 从使用系统的人的角度来看,可视化完成给定目标所需的工作流程有助于识别我们系统的关键功能。

Figure 14.3 Visualizing the workflows required to accomplish a given goal helps to identify the critical capabilities of our system, as seen from the perspective of a person using the system.

值得注意的是,尽管我们才刚刚开始制作旅程地图,但我们已经在使用它来识别高级工作流程不同部分之间的界限。在这种情况下,我们可以将实现预订航班目标所需的工作流程视为由两个独立且相当独立的活动序列组成:查找航班和进行预订。我们之所以想将它们视为独立的,是因为参与者正在寻找航班并不一定意味着他们想要进行预订。他们可能是想查看出发和到达时间或比较价格。航班预订工作流程也是如此。虽然参与者可以使用航班查找器功能预订他们找到的航班,但他们可能在点击广告或新闻稿中的链接后立即进入预订工作流程,因此完全绕过了航班查找器。

It’s important to note that even though we’ve only just started working on our Journey Map, we’re already using it to identify the boundaries between the different parts of high-level workflows. In this case, we can think of the workflow required to accomplish a goal of booking a flight as consisting of two separate and reasonably independent sequences of activities: to find a flight and to make a booking. The reason we’d want to think of them as separate is that because an actor is looking for a flight doesn’t necessarily mean that they want to make a booking. It could be that they want to check the departure and arrival times or compare its price. The same goes for the flight-booking workflow. While the actor could book a flight they found using the flight-finder feature, they might have entered the booking workflow right after clicking on an ad or a link in the newsletter, therefore bypassing the flight finder altogether.

注意:了解不同工作流程之间的界限对于设计可重用的测试代码至关重要。

NOTE Understanding where the boundaries lie between the different workflows is critical in designing reusable test code.

一旦你理解了实现给定目标所需的工作流程,就值得询问参与者在开始他们的第一个工作流程之前需要执行的任何其他活动(即他们需要满足的任何先决条件)。这些知识在设计自动化测试时变得非常有价值,因为它可以帮助你更好地理解你需要考虑的任何测试数据和运行时依赖关系。

Once you understand the workflows required to accomplish a given goal, it’s always worth asking about any other activities an actor would need to perform before they could start with their first workflow (i.e., any prerequisites they need to meet). Such knowledge becomes invaluable when designing automated tests as it helps you get a better understanding of any test data and runtime dependencies you’ll need to consider.

同样,了解对其他工作流程的依赖关系的一个好方法是要求业务利益相关者或提供系统概述的人员逐步向您展示如果他们自己执行第一个工作流程,他们将如何启动它。例如,如果您听说给定的工作流程“不与第三方协调”、“锁定测试环境以避免其他测试人员的干扰”或“运行通宵批处理来设置测试数据”就无法完成,您就会立即知道您需要将解决这些问题纳入您的测试自动化方法中。

Here, again, a good way to learn about any dependencies on other workflows is to ask the business stakeholder, or the person providing the overview of the system, to show you step by step how they’d initiate the first workflow if they were to do it themselves. If you hear, for example, that a given workflow can’t be done “without coordinating with a third party,” “locking down the test environment to avoid interference from other testers,” or “running an overnight batch process to set up the test data,” you’ll know right away that you need to incorporate solving those problems into your test automation approach.

例如,在 Flying High Airlines 的案例中,系统仅支持经过身份验证的旅行者。这意味着要使用该系统,未经身份验证的旅行者需要登录,而没有帐户的旅行者则需要先注册。我们扩展了旅程地图以捕获这些新信息,如图 14.4 所示。

For example, in the case of the Flying High Airlines, the system supports only authenticated travelers. This means that to use the system a traveler who’s not authenticated would need to sign in, and a traveler who doesn’t have an account would need to sign up first. We expand our Journey Map to capture this new information as shown in figure 14.4.

图 14.4 向旅程地图添加先决条件工作流程有助于确定自动化测试需要采取的任何其他步骤。

Figure 14.4 Adding prerequisite workflows to the Journey Map helps to identify any additional steps an automated test will need to take.

从执行者完成各种工作流程的角度来思考系统,将有助于您快速了解系统试图解决的问题、解决问题的方法以及所需的任何依赖项。它还可以帮助您识别业务关键型工作流程,并专注于自动化测试场景,这些场景涵盖了系统中对利益相关者最重要的部分。

Thinking about the system from the perspective of an actor completing various workflows will help you to quickly build an understanding of the problems the system is trying to solve, the ways it does it, and any dependencies it requires. It will also help you to identify business-critical workflows and focus on automating test scenarios that cover those parts of the system that matter the most to its stakeholders.

此外,这种方法还可以帮助您设计可重复使用的测试代码,并尽早发现优化机会。例如,虽然测试参与者需要完成注册和登录的工作流程,然后才能找到航班并进行预订,但他们也许可以使用更高效的方法(如 API 调用或播种测试数据库)来完成此操作,而不是使用 Web 浏览器这种较慢的方法。我们将在下一节 15.1.5 中研究混合测试时详细讨论这一点章。

Furthermore, this approach will also help you design reusable test code and spot opportunities for optimizations sooner. For example, while the test actor is required to complete the workflows to sign up and sign in before they can find their flight and make a booking, perhaps they could do so using a more efficient method like an API call or seeding the test database instead of the slower method of using a web browser. We’ll talk more about this when we investigate blended testing in section 15.1.5 in the next chapter.

14.1.3 将工作流与功能关联

14.1.3 Associate workflows with features

一次你已经确定了实现参与者目标所需的高级工作流,下一步是将这些工作流按逻辑分组,并将它们与支持这些工作流的高级系统功能关联起来。你可以在图 14.5 中看到这种关联。

Once you’ve identified the high-level workflows necessary to accomplish the actor’s goal, the next step is to group such workflows logically and associate them with the high-level system features that enable those workflows. You can see this association depicted in figure 14.5.

图 14.5 系统功能与其启用的工作流相关。请注意,为了实现目标,参与者通常需要与多个系统功能进行交互。

Figure 14.5 System features associated with the workflows they enable. Note that to accomplish a goal an actor typically needs to interact with multiple system features.

可视化以参与者为中心的工作流程与以系统为中心的功能(如注册、身份验证、航班查找器或预订)之间的这种高级关联,将有助于您更好地了解为给定工作流程提供充分覆盖所需的自动化测试范围。它还为您可能希望纳入测试策略的其他类型和级别的测试(包括自动化测试和探索性测试)划定了方便的边界。

Visualizing this high-level association between actor-centric workflows and system-centric features like registration, authentication, flight finder, or bookings will help you to better understand the scope of the automated tests needed to provide sufficient coverage of a given workflow. It also demarcates a convenient boundary for other types and levels of testing you might want to incorporate in your testing strategy, both automated and exploratory.

您会发现,通常任何给定的高级功能都会启用多个工作流程,如图 14.6 所示。例如,身份验证功能启用登录工作流程,但也启用重置密码工作流程。航班查找器可能会为其用户提供记住他们最喜欢的目的地的选项;预订功能可能允许参与者在预订中添加额外服务、重新安排航班或申请退款。就像您发现的任何新参与者和目标一样,将这些额外的工作流程添加到您的旅程地图中也很有用。但是,请尝试在此发现练习的广度和深度之间保持适当的平衡,并注意不要试图预先规划出系统支持的每个工作流程。

You’ll find that typically any given high-level feature enables multiple workflows, as shown in figure 14.6. For example, the authentication feature enables a sign-in workflow, but also a reset password workflow. The flight finder might offer its users an option to remember their favorite destinations; the bookings feature might allow the actors to add extra services to their booking, reschedule a flight, or request a refund. Just like with any new actors and goals you discover, here it also could be useful to add those additional workflows to your Journey Map. However, try to maintain the right balance between breadth and depth of this discovery exercise and be mindful not to attempt to map out every single workflow the system supports upfront.

图 14.6 典型的软件系统支持多个参与者,具有多个目标,并提供支持数十种工作流程的功能。不要试图预先规划整个系统,而是首先关注那些支持您已确定的高优先级参与者目标的工作流程。

Figure 14.6 A typical software system supports multiple actors, with multiple goals, and offers features enabling dozens of workflows. Instead of trying to map out the entire system upfront, focus first on those workflows that support the high-priority actor goals you’ve identified.

如前面的图 14.1 所示,一个好方法是从一个或少数几个参与者及其目标开始,仅确定实现这些目标所需的关键工作流程,并完成自动化测试自动化周期,然后再对旅程地图进行迭代。另一种特别有助于帮助您保持正轨的技术是建立一条钢线,我们将对此进行调查下一个。

As shown in figure 14.1 earlier, a good approach is to start with one or a handful of actors and their goals, identify only the critical workflows required to accomplish those goals, and complete the automation test automation cycle before iterating on the Journey Map again. Another technique that’s particularly useful in helping you to stay on track is to establish a steel thread, which we’ll investigate next.

14.1.4 建立展示特征的场景的钢丝

14.1.4 Establish a steel thread of scenarios that demonstrate the features

通常,任何给定的高级功能都支持多个参与者工作流,每个利用给定功能的工作流都有多个使用场景,并且这些场景倾向于支持众多业务规则。

Typically, any given high-level feature supports multiple actor workflows, each workflow utilizing a given feature has multiple usage scenarios, and those scenarios tend to support numerous business rules.

由于我们创建的软件系统本身就很复杂,因此很容易陷入细节,并花费大量时间来开发自动化测试,而这些测试对于参与者实现其目标而言不一定是关键路径。为了确保您走在正确的轨道上,从钢丝开始实施我们的自动化测试套件是很有用的。

Because of this inherent complexity of the software systems we create, it is easy to get bogged down in details and spend a significant amount of time developing automated tests for features that are not necessarily on the critical path of the actor accomplishing their goal. To make sure you stay on track, it’s useful to start the implementation of our automated test suite from a steel thread.

就像桥梁建造者过去在山谷上建造桥梁时所做的那样,你不会从整座桥梁的 5 英尺长部分开始,因为桥梁会因失去支撑而倒塌。你从一条简单的缆绳开始,即一根钢线,然后用它拉过第一根窄梁。然后将其他部件连接到该梁上,直到桥建成。

Just like what the bridge builders used to do when constructing a bridge across a valley, you don’t start with a 5-foot piece of the whole bridge, as the bridge would fall down, unsupported. You begin with a simple cable right across, a steel thread, which you then use to pull over the first narrow beam. Then you attach other parts to that beam until you have a bridge.

这与我们为现有系统实施验收测试时所做的类似,但在这里,我们不是搭建桥梁,而是专注于在每个功能中执行参与者实现其目标所需的绝对最少场景。

This is similar to what we do when implementing acceptance tests for an existing system, but here, rather than building a bridge, we focus on exercising the absolute minimum of scenarios within each feature required for the actor to achieve their goal.

在这些场景中,端到端地测试系统也很重要。但是,请记住,与流行的观点相反,“端到端”并不意味着通过 Web 界面对完全组装和部署的系统运行测试。远非如此。当我们谈论端到端时,我们指的是工作流程的末端,关注的是它的广度而不是深度。虽然您希望从头到尾测试工作流程,在我们的例子中是从用户注册到登录、查找航班和进行预订,但您可以选择针对其 API 而不是 Web UI 执行部分甚至整个工作流程,或者针对内存数据库而不是类似生产的数据库服务器。

It is also important for those scenarios to exercise the system end to end. However, bear in mind that here, and contrary to popular opinion, “end to end” doesn’t mean running the tests against a fully assembled and deployed system through its web interface. Far from it. When talking about end to end we mean the ends of the workflow, focusing on its breadth rather than its depth. While you’ll want to exercise the workflow from its beginning to its end, in our case from user sign up through sign in, finding a flight, and making a booking, you could choose to perform parts or even the whole workflow against its APIs not the Web UI, or against in-memory databases rather than a production-like DB server.

这种建立钢线的技术在软件架构中广为人知,在测试自动化中也有很多好处。它可以让您了解自己对系统应该如何工作的整体理解是否正确,并有助于揭示您和您的团队可能做出的任何假设,这些假设通常是在不知不觉中做出的。它还可以帮助您了解团队或业务赞助商可能没有意识到的任何依赖关系,而且由于您能尽早发现它们,因此您可以有更多时间对它们采取行动。

This technique of establishing a steel thread is well known in software architecture and has many benefits in test automation too. It allows you to see whether your overall understanding of how the system is supposed to work is correct and helps to surface any assumptions you and your team might be making, often without realizing it. It will also help you see any dependencies that the team or the business sponsors might not have been aware of, and since you detect them early it will give you more time to do something about them.

所有这些都提高了您测试自动化工作成功的机会,因为它为您创造了机会,让您能够更快地做出反应,在风险变成问题之前解决它们。它还可以帮助您争取时间来更改自动化测试套件的架构或被测系统本身。这种早期反馈机制还可以帮助您避免一种不幸但常见的情况,即依赖关系在数周或数月内都未被发现,直到最后一刻才出现,例如,当您意识到,即使您已经构建了一个最先进的测试自动化系统,设置运行所需的测试数据也是一个手动过程,需要数周时间,并且需要由您无法控制的另一个团队执行。

All that improves your chances of being successful with your test automation effort, as it creates opportunities for you to react sooner and address risks before they become problems. It also helps you buy time to make changes to the architecture of your automated test suite, or the system under test itself. This early feedback mechanism can also help you to avoid an unfortunate yet common scenario where dependencies go undetected for weeks or months, only to appear at the last minute when you realize that, for example, even though you’ve built a state-of-the-art test automation system, setting up the test data it needs to run is a manual process that takes weeks and needs to be performed by a different team that you don’t control.

如何建立需要执行的最小场景集,以确保系统允许参与者实现其目标?您可以使用特征映射的变体(这是您在第 6.5 节中第一次遇到的一种技术),并从在每个特征中寻找最简单的成功场景开始,这些场景允许参与者朝着预订的目标前进。

How do you establish the minimum set of scenarios you need to exercise to ensure that the system allows the actor to accomplish their goal? You use a variation of Feature Mapping, a technique you first encountered in section 6.5, and start from finding the simplest possible successful scenario within each feature that allows the actor to progress toward their goal of making a booking.

例如,完成注册流程的最简单方法是使用有效的旅行者详细信息进行注册,并使用基于电子邮件的注册流程,而不是需要与第三方交互的流程。为了通过登录流程而不受任何表单验证规则的干扰,参与者需要提供有效的身份验证凭据。为了找到允许参与者预订的航班,参与者需要搜索最简单的旅程配置,例如经济舱的单程直飞航班。图 14.7 中我们更新的旅程地图显示了这组最简单的成功场景。

For example, the easiest way to get through the sign-up workflow is to sign up with valid traveler details and use an email-based registration workflow rather than one that requires interacting with a third party. To pass the sign-in workflow without any fuss from any form validation rules, the actor needs to provide valid authentication credentials. To find a flight that will let the actor book it, the actor needs to search for the simplest possible journey configuration, such as a one-way direct flight in an economy class. This minimal set of simplest possible successful scenarios is shown on our updated Journey Map in figure 14.7.

图 14.7 建立实现给定目标所需的最简单可能场景的钢丝

Figure 14.7 Establishing a steel thread of simplest possible scenarios required to accomplish a given goal

您可能已经注意到,在图 14.7 中,到目前为止,我们的钢线仅涵盖了最简单的“快乐路径场景”,即在提供有效输入时确认系统按预期工作的积极案例。当然,这只为我们提供了系统提供的功能的基本覆盖。然而,这是一个很好的起点,可以让我们更轻松地改进我们的自动化测试套件,同时确保最关键的工作流程始终有一个可靠的安全网。

You might have noticed in figure 14.7 that, so far, our steel thread covers only the simplest possible “happy path scenarios,” those positive cases confirming the system works as expected when provided with valid inputs. Of course, this gives us only rudimentary coverage of the functionality offered by the system. It is, nevertheless, a great place to start and makes it easier for us to evolve our automated test suite while ensuring that there’s always a reliable safety net around the most critical workflows.

这种方法的另一个优点是,它提供了一个思维模型,可以帮助我们避免被参与者可能通过系统采取的所有可能路径的组合爆炸所淹没。相反,我们将在每个单独功能的更易于管理的范围内考虑这种可变性。我们不会忽略验收测试中的“不愉快路径场景”,我们将讨论如何将它们纳入其中不久。

Another advantage of this approach is that it offers a mental model that helps us to avoid getting overwhelmed by the combinatorial explosion of all the possible paths an actor could take through the system. Instead, we will consider this variability within a much more manageable scope of each individual feature. We won’t ignore the “unhappy path scenarios” in our acceptance tests, and we’ll talk about how to incorporate them shortly.

14.1.5 确定每个场景的可验证后果

14.1.5 Determine verifiable consequences of each scenario

一次你已经完成了建立被测系统的目的、确定与之交互的一些参与者、他们想要实现的目标、实现这些目标的工作流程以及在此过程中执行关键功能的一系列场景,现在是时候考虑参与者将执行的具体活动了。值得庆幸的是,我们的旅程地图已经包含了重要工作流程的列表,以及我们需要更详细调查的这些工作流程中的场景。让我们从注册工作流程开始,因为其他工作流程都依赖于它。

Once you’ve gone through the exercise of establishing the purpose of the system under test, identifying some of the actors who interact with it, the goals they want to accomplish, the workflows that enable achieving those goals, and a steel thread of scenarios to exercise the critical features along the way, it’s time to think about the specific activities the actors would perform. Thankfully, our Journey Map already contains a list of important workflows, as well as scenarios within those workflows that we’ll need to investigate in more detail. Let’s start with the sign-up workflow since other workflows depend on it.

捕获工作流以便将其转变为自动化测试的最简单方法可能有点违反直觉:我们从结果开始,然后反推。我们如何知道给定的工作流是否成功?我们需要验证哪些后果来确定这一点?另一方面,我们如何知道给定的工作流是否成功?回答这些问题有助于我们建立一个心理模型,以了解系统对参与者提供的刺激将产生的可观察反应。它还将帮助我们思考我们的自动化测试需要执行的断言。

The easiest way to capture a workflow so that it can be then turned into an automated test is perhaps slightly counterintuitive: we’ll start from the outcome and then work our way back. How would we know if a given workflow was successful? What consequences would we need to verify to determine that? And on the other hand, how would we know if a given workflow was not successful? Answering those questions helps us build a mental model of the observable responses the system will produce to the stimuli the actors provide. It will also help us think about the assertions our automated tests need to perform.

在注册工作流程的情况下,很容易识别预期的结果:当旅行者的帐户创建时,工作流程成功完成,参与者可以继续登录工作流程,如图 14.8 所示。由于我们还不确定参与者需要执行哪些具体任务才能完成此结果,因此我们在旅程地图中添加了占位符卡并将其标记为 TBC 或待确认。

In the case of the sign-up workflow it’s easy to identify the expected consequences: the workflow is completed successfully when the traveler’s account is created, and the actor can move on to the sign-in workflow, as depicted in figure 14.8. Since we’re not yet sure what exact tasks an actor needs to perform this outcome, we add a placeholder card to our Journey Map and mark it as TBC, or to be confirmed.

图 14.8 使用特征映射中的约定可视化工作流

Figure 14.8 Visualizing a workflow using conventions from Feature Mapping

在可视化每个场景时,我们使用颜色约定,类似于您在第 6 章(第 6.5 节)学习特征映射时看到的颜色约定:板左侧的白色卡片表示工作流程,绿色卡片描述场景,紫色卡片表示后果。中间的黄色卡片,尽管目前只包含基本的注释,但很快就会扩展到描述参与者为实现给定结果需要执行的确切活动。

When visualizing each scenario, we use color convention, similar to what you saw in chapter 6 (section 6.5) when you learned about Feature Mapping: white cards on the left of the board signify the workflows, green cards describe the scenarios, and purple cards are the consequences. The yellow cards in the middle, even though they contain only rudimentary notes for now, will soon expand to describe the exact activities an actor needs to perform in order to achieve a given outcome.

值得注意的是,虽然到目前为止我们只关注了一些快乐路径场景,但我们的旅程地图也不应该忽视所谓的不快乐路径场景。那些负面情况,事情出错,错误发生,参与者无法实现他们的目标。例如,积极注册场景的负面变化是参与者不被允许继续登录,可能是因为他们提供了无效的注册详细信息。在这种情况下,注册表单验证规则应该启动,帐户不应该被注册,并且应该建议参与者如何解决注册过程中的任何问题(见图 14.9)。

It’s important to observe that while so far we have focused on a handful of happy path scenarios, our Journey Map should not neglect the so-called unhappy path scenarios either. Those negative cases where things go wrong, errors occur, and the actor is prevented from achieving their goal. For example, a negative variation of a positive sign-up scenario is one where the actor is not allowed to proceed to sign-in, perhaps because they’ve provided invalid registration details. In this case, the sign-up form validation rules should kick in, the account should not get registered, and the actor should be advised on how to fix any problems with their registration (see figure 14.9).

期望与现实

Expectations vs. reality

请注意,在描述每个场景的结果时,我们倾向于使用“应该”一词。这是因为我们从预期结果(交付团队及其业务赞助商所理解的期望结果)开始可视化每个场景。根据我们的经验,当涉及到复杂的遗留系统时,预期结果有时可能与实际结果不一致。

Note that when describing the outcomes of each scenario we tend to use the word “should.” This is because we visualize each scenario starting from its expected outcome, the desired result as understood by the delivery team and its business sponsors. In our experience, when it comes to complex legacy systems, the expected outcome might sometimes be at odds with the actual outcome.

确定系统预期要做什么以及每个工作流程预期会产生什么结果,往往是一个比简单地在自动化测试中反映系统当前行为更好的起点,因为它引导我们建立对期望结果的共同理解。此策略还可以帮助您突出显示、讨论和协调预期状态和实际状态之间的任何潜在不一致之处,而不是冒险将系统中的任何缺陷和误解的需求复制到测试套件中。

Establishing what a system is expected to do, and what each workflow is expected to yield, tends to be a better starting point than simply mirroring the current behavior of the system in your automated tests as it steers us toward building a shared understanding of the desired outcome. This strategy will also help you to highlight, discuss, and reconcile any potential inconsistencies between the expected and actual state instead of risking to replicate any defects and misunderstood requirements from the system into your test suite.

图 14.9 将幸福路径场景和不幸福路径场景可视化,以突出它们的不同后果。

Figure 14.9 Visualize both happy and unhappy path scenarios to highlight their different consequences.

就像在特征映射中一样,在这里我们也使用蓝色索引卡来记录我们在此过程中发现的任何业务规则,并使用粉色卡片来记录任何未解答的问题或在探索性测试中考虑的主题。

Just like in Feature Mapping, here too we use blue index cards to record any business rules we discover along the way and pink cards to capture any unanswered questions or topics to consider in exploratory testing.

在我们的旅程地图上捕获业务规则有助于我们了解给定工作流程的复杂性,而场景与规则卡的比例则表明了其可变性。捕获业务规则还有另一个优势,因为它为我们提供了在实施自动化测试时“按规则拆分”的机会。这意味着,我们不必尝试一次性自动化给定工作流程的所有场景,而是可以按它们支持的业务规则对它们进行划分,并以更小的批次实施,一次实施一条业务规则(见图 14.10)。

Capturing business rules on our Journey Map helps us get an indication of the complexity of a given workflow, and the ratio of scenario to rule cards is an indication of its variability. Capturing business rules also has another advantage as it offers us an opportunity to “split by rule” when implementing the automated tests. This means that instead of trying to automate all the scenarios of a given workflow in one go, we could instead divide them by business rules they support and implement in much smaller batches, one business rule at a time (see figure 14.10).

图 14.10 可视化业务规则有助于我们评估工作流程的复杂性,并发现通过业务规则划分自动化工作的机会。

Figure 14.10 Visualizing business rules helps us gauge the complexity of a workflow and spot opportunities to split automation work by business rule.

正如您所注意到的,我们并没有深入研究参与者需要执行哪些活动才能达到给定结果的细节,而是满足于一些基本的注释,如黄色任务卡上所示。然而,这正是我们所需要的,因为在这个阶段,我们感兴趣的是列举给定工作流程中的场景及其结果,并找出它们需要遵守的任何业务规则。我们将研究参与者究竟如何实现每个场景的结果下一个。

As you’ve noticed, we haven’t dived too deep into the details of the activities an actor would need to perform to reach a given consequence, and instead we’ve settled for some rudimentary notes, as seen on the yellow task cards. However, that’s exactly what we need since at this stage we are interested in enumerating scenarios and their outcomes within a given workflow and spotting any business rules they need to obey. We’ll investigate how exactly the actors would go about reaching the outcomes of each scenario next.

14.1.6 使用任务分析来理解每个场景的步骤

14.1.6 Using task analysis to understand the steps of each scenario

现在了解给定工作流程应导致的结果后,下一步是分析参与者需要执行哪些高级活动才能实现这些结果。要使用现有系统做到这一点,我们需要对其进行一些实验,并了解它如何响应我们提供的刺激。

Now that we understand what outcomes a given workflow should lead to, the next step is to analyze the high-level activities the actor needs to perform to reach those outcomes. To do that with an existing system, we will need to experiment with it a bit and learn about how it responds to the stimuli we provide.

在使用基于 Web 的用户界面时,最好保持您最喜欢的 Web 浏览器的开发人员工具控制台处于打开状态,以查看前端和后端组件之间交换的任何请求(见图 14.11)。这将帮助您发现让测试直接与 API 交互的机会,我们将在第 15 章研究混合测试时详细讨论这一点。

When working with web-based user interfaces, it’s also a good idea to keep the developer tools console of your favorite web browser open to see any requests exchanged between the frontend and the backend components (see figure 14.11). This will help you spot opportunities to make your tests interact directly with the APIs, which we’ll talk more about when we investigate blended testing in chapter 15.

图 14.11 提交注册表单会导致 Web UI 向后端发出 POST 请求。这表明,除了填写表单外,自动化测试还可以直接向服务器发送 HTTP 请求以达到相同的结果。

Figure 14.11 Submitting the registration form results in a POST request from the web UI to the backend. This suggests that instead of filling out the form, an automated test could also send an HTTP request directly to the server to arrive at the same outcome.

在 Flying High 应用程序的案例中,我们调查的注册工作流程相当简单,只需要参与者执行单个高级任务即可通过 Web UI 注册,如图 14.12 所示。此任务由几个子任务组成,参与者需要找到注册表单、填写并提交,同时确保提交成功。如果显示确认操作成功结果的通知,则参与者可以假定他们可以继续登录工作流程。

In the case of the Flying High app, the sign-up workflow we investigate is rather simple and requires the actor to perform a single high-level task to sign up via web UI, as shown in figure 14.12. This task consists of several subtasks for the actor to locate the registration form, fill it out, and submit it while also ensuring that the submission was successful. If the notification confirming a successful result of the operation is shown, the actor can assume that they can proceed with the sign in workflow.

图 14.12 “使用有效的旅行者详细信息进行注册”场景包含一个通过 Web UI 注册的高级任务,并允许旅行者在所有三个子任务成功完成后继续登录工作流程。

Figure 14.12 The “Sign up using valid traveler details” scenario consists of a high-level task to sign up via Web UI and allows the traveler to proceed to the sign-in workflow when all three subtasks are completed successfully.

当然,构成注册任务的每项活动都可以使用相同的模式进一步细分。每个较高级别的活动都成为一系列较低级别活动(如何)的迷你目标(什么)。找到注册表单、填写、提交并确认提交成功,参与者就可以完成注册账户的目标。导航到 Flying High 主页并单击注册链接,参与者就可以完成找到注册表单的目标,依此类推。如果我们从高级领域特定概念(注册)到低级实现特定交互(单击按钮并在表单字段中输入文本)一直遵循相同的模式,我们最终可能会得到如图 14.13 所示的旅程地图的一部分。

Of course, each of the activities that constitute the task to sign up could be broken down further using the same pattern. Each higher-level activity becomes a mini-goal (the what) of a sequence of lower-level activities (the how). Locating the registration form, filling it out, submitting it, and confirming that the submission was successful allows the actor to accomplish the goal of registering an account. Navigating to the Flying High homepage and clicking on the link to register allows the actor to accomplish the goal of locating the registration form and so on and so forth. If we followed the same pattern all the way from the high-level domain-specific concept of signing up to low-level implementation-specific interactions of clicking on buttons and entering text into form fields we might end up with a part of a Journey Map that looks like figure 14.13.

图 14.13 旅程地图的一个分支展开以突出显示注册工作流程中涉及的任务。请注意,为了适合一页,工作流程的任务是垂直布局而不是水平布局。这两种布局都是有效的,您可以交替使用它们来处理您可用的媒介,例如物理或虚拟白板、计算机屏幕或墙上的便签。

Figure 14.13 One of the branches of the Journey Map expanded to highlight the tasks involved in the workflow to sign up. Note that to fit on a page the tasks of the workflow are laid out vertically rather than horizontally. Both layouts are valid, and you can use them interchangeably to work with the medium you have available, like a physical or virtual whiteboard, a computer screen, or sticky notes on a wall.

这种将工作流中涉及的活动呈现为任务和子任务层次结构的方法基于用户体验研究领域中称为分层任务分析(HTA)的技术)。如您所见,即使是在较短的工作流中相对简单的场景(例如注册)也可能涉及相当多的低级活动。如果我们像图 14.13 中指定称呼的任务一样扩展所有特定任务,我们最终将得到十几个低级交互!当然,我们的自动化测试通过基于 Web 的界面至少执行一次此工作流是非常有意义的。毕竟,这是与我们的系统交互的实际最终用户将使用的界面。但是,每次场景要求测试参与者注册时都完全执行它,这是相当浪费的。

This approach of presenting activities involved in a workflow as a hierarchy of tasks and subtasks is based on a technique known in the field of user experience research as Hierarchical Task Analysis (HTA). As you can see, even a relatively simple scenario within a short workflow like the one to sign up can involve quite a few lower-level activities. If we were to expand all the specific tasks just like we did with the one to specify salutation in figure 14.13, we’d end up with over a dozen low-level interactions! Of course, it makes perfect sense for our automated tests to exercise this workflow at least once through the web-based interface. After all, this is the interface that the actual end users interacting with our system will use. However, exercising it in full every single time a scenario requires the test actor to be signed up is rather wasteful.

值得庆幸的是,我们在手动执行工作流程时打开了开发人员工具控制台,并且我们知道可以通过更简单的方式实现注册帐户的相同结果。也就是说,我们观察到系统的前端组件使用 HTTP POST 请求将填写好的表单提交给注册 API。这正是我们的自动化测试在那些我们希望关注参与者注册的结果而不是让他们注册的基于 Web 的交互的场景中可以做的事情,如图 14.14 所示。

Thankfully, we had the developer tools console open when we performed the workflow manually, and we know that the same outcome of registering an account could be achieved by much simpler means. Namely, we’ve observed that the frontend component of our system submits the filled-out form to the registration API using an HTTP POST request. This is exactly what our automated tests could do in those scenarios where we want to focus on the outcome of the actor getting registered rather than the web-based interactions that get them there, as shown in figure 14.14.

图 14.14 通过向旅行者注册 API 提交单个 POST 请求即可完成注册任务。

Figure 14.14 The task to sign up could be accomplished by submitting a single POST request to the traveler registration API.

通过与被测系统的不同接口交互可以实现相同的结果,这一观察结果在我们讨论混合测试时很有用第 15 章。

This observation that the same outcome can be achieved by interacting with different interfaces of the system under test will be useful when we talk about blended testing in chapter 15.

14.2 设计可扩展的测试自动化系统

14.2 Designing scalable test automation systems

现在我们已经很好地了解了需要自动化的第一个工作流程以及我们可以与之交互的系统的不同接口,现在让我们来谈谈如何处理测试自动化本身的过程。当然,我们追求的最终目标是测试套件,它可以帮助我们验证系统是否满足并继续满足测试场景中表达的验收标准。此外,我们希望这样的测试套件能够提供一个基础,帮助我们测试未来添加到系统中的任何新功能。

Now that we have a good understanding of the first workflows we need to automate and the different interfaces of our system that we can interact with, let’s talk about how to approach the process of test automation itself. Of course, the end goal we’re after is a test suite that helps us verify if the system meets and continues to meet its acceptance criteria as expressed in our test scenarios. Furthermore, we’d like such a test suite to provide a foundation to help us test-drive any new functionality added to the system in the future.

然而,就像生活中的许多其他事情一样,我们对自动化测试的看法决定了我们对待它们的方式,我们对待它们的方式决定了它们会变成什么样子。由于我们的目标是拥有快速、可靠、高质量的测试来支持而不是阻碍我们的工作,因此我们需要摆脱开发人员和测试人员通常对它们的看法。

However, just like with many other things in life, the way we think about our automated tests is the way we treat them, and the way we treat them is what they become. Since our goal is to have fast, reliable, high-quality tests that support rather than obstruct our work we need to depart from the way they’re often viewed by developers and testers alike.

我们行业中仍然普遍持有这样一种观点:自动化验收测试不过是一堆不复杂的脚本 — — 它们看起来像意大利面条式的低级交互代码,没有业务领域含义。不幸的是,即使是使用 Cucumber 等可执行规范工具的团队也经常尝试直接在其步骤定义库中实现此类低级交互序列,最终导致测试缓慢、不稳定,更不用说难以理解和重复使用的测试代码,无法带来可靠测试自动化的好处。

The view still often held in our industry is that automated acceptance tests are nothing more than a bunch of unsophisticated scripts—that it’s fine for them to look like spaghetti code of low-level interactions, devoid of business domain meaning. Unfortunately, even teams using executable specification tools like Cucumber will often try to implement such sequences of low-level interactions directly in their step definition libraries and end up with slow, flaky tests, not to mention difficult-to-understand and reused test code that fails to yield the benefits of reliable test automation.

有什么替代方案?设计可靠的测试自动化任务的更好方法不是从编写测试的角度,而是从创建测试自动化系统的角度,因此我们开发了一个专门的支持系统来验证和记录被测系统的行为。为此,我们需要将测试场景视为更大整体的一部分,并在它们运行的​​环境中考虑它们,包括它们使用的框架和库,以及将它们集成在一起的任何自定义粘合代码。

What’s the alternative? A better way to look at the task of designing reliable test automation is not from the perspective of writing tests but from the perspective of creating a test automation system, so a specialized supporting system we develop to verify and document the behavior of the system under test. To do that, we need to think about test scenarios as just one piece of a larger whole and consider them in the context in which they operate, including the frameworks and libraries they use, and any custom glue code that integrates it all together.

从思考自动化测试或测试脚本转向思考测试自动化系统是有益的,因为一旦我们开始以系统的角度思考,我们就应该看到与我们作为一个行业已经学到的软件系统架构、领域建模、功能组合或设计模式相似之处。将这些经验教训应用到测试自动化系统的构建中,将帮助您和您的团队创建快速、可靠的测试,并设计高质量、易于维护的测试代码,这些代码不仅可以支持您的团队,还可以支持您组织内的其他团队。

The reason it’s beneficial to shift from thinking about automated tests or test scripts to thinking about test automation systems is because as soon as we start thinking in terms of systems we should see parallels with what we’ve already learned as an industry about software systems architecture, domain modeling, functional composition, or design patterns. Applying those lessons to how you structure your test automation systems will help you and your team create fast, reliable tests and design high-quality, easy-to-maintain test code that scales to support not only your team, but also other teams within your organization.

14.2.1 使用分层架构设计可扩展的测试自动化系统

14.2.1 Using layered architecture to design scalable test automation systems

只是就像着手设计生产系统的任务一样,设计测试自动化系统的一个好方法是遵循一组明确定义的原则,一组指导方针,帮助我们决定如何构建代码以及在不同组件的职责之间划定界限。

Just like when approaching the task of designing a production system, a good way to approach designing a test automation system is to follow a well-defined set of principles, a set of guidelines that help us decide how to structure our code and where to draw boundaries between the responsibilities of the different components.

适合设计测试自动化系统的软件架构风格称为分层或n 层架构(见图 14.15)。虽然在典型的生产系统中,这些层可能代表用户界面、应用程序逻辑和数据层,但基于 Serenity/JS 测试自动化框架的测试自动化系统通常具有三个主要层:

A software architecture style that lends itself well to designing test automation systems is called layered, or n-tier architecture (see figure 14.15). While in a typical production system those layers might represent user interface, application logic, and data layers, test automation systems based on Serenity/JS test automation framework typically have three main layers:

  • 规范层。规范层负责捕获系统利益相关者认为重要的工作流、业务规则和使用示例。此层是测试自动化系统与接受被测系统功能的人员之间的接口。虽然 Serenity/JS 使我们能够使用流行的测试运行器(例如 Mocha、Jasmine 或 Playwright)实现规范层,但使用 Cucumber 可以让我们更轻松地捕获有关给定功能的业务环境的其他信息。它还通过提供一种方便的方式,以英语以外的语言(例如法语、西班牙语或德语)表达规范,从而提高了我们自动化测试的可访问性。

  • Specification layer. The Specification layer is responsible for capturing the workflows, business rules, and usage examples that the stakeholders of the system find important. This layer is the interface between the test automation system and the people accepting the functionality of the system under test. While Serenity/JS enables us to implement the Specification layer using popular test runners, such as Mocha, Jasmine, or Playwright, using Cucumber makes it easier for us to capture additional information about the business context of a given feature. It also improves the accessibility of our automated tests by offering a convenient way for specifications to be expressed in languages other than English—for example French, Spanish, or German.

  • 领域层。领域层使用来自集成层的代码和 Serenity/JS Screenplay 模式 API,并引入额外的抽象来模拟完成规范层中表达的工作流所需的确切活动。它还负责将业务含义与较低级别活动的序列关联起来,正如您将在第 15 章中看到的那样。

  • Domain layer. The domain layer uses code and Serenity/JS Screenplay pattern APIs from the Integration layer and introduces additional abstractions to model the exact activities required to complete the workflows expressed in the Specification layer. It is also responsible for associating business meaning with sequences of lower-level activities, as you’ll see in chapter 15.

  • 集成层。集成层弥合了业务领域和技术领域之间的鸿沟。它将领域层中表达的参与者的活动转换为与被测系统特定外部接口的精确低级交互。这包括与测试工具集成、识别可交互元素、设置测试数据、测试报告等。

  • Integration layer. The Integration layer bridges the gap between the business domain and the technology domain. It translates actors’ activities expressed in the Domain layer to exact low-level interactions with the specific external interfaces of the system under test. This includes integration with test tools, identifying interactable elements, setting up test data, test reporting, and so on.

图 14.15 基于 Serenity/JS 的典型测试自动化系统的层次及其内容

Figure 14.15 Layers of a typical test automation system based on Serenity/JS, with their contents

任何分层架构的基本原则是,给定层的元素仅依赖于同一层中的其他元素或属于其下一层的元素。层的价值在于,每个层都专注于测试自动化系统的特定方面。这种专业化允许对每个方面进行更具凝聚力的设计,并使这些设计更容易理解。

The essential principle of any layered architecture is that an element of a given layer depends only on other elements in the same layer, or elements that belong to the layer directly below it. The value of layers is that each one specializes in a particular aspect of the test automation system. This specialization allows for more cohesive design of each aspect and makes these designs much easier to understand.

许多测试套件中经常出现的一个常见设计错误是跳过域层,直接在规范层中调用低级集成 API(例如 Selenium WebDriver API)。这会将规范层与特定于接口的交互和工具绑定在一起,并成为引入更高级技术(例如混合测试或可重用测试代码)的障碍。

A common design error often seen in many test suites is to skip the Domain layer and invoke low-level integration APIs, such as Selenium WebDriver APIs, directly in the Specification layer. This ties the Specification layer with interface-specific interactions and tools and acts as an obstacle to introducing more advanced techniques such as blended testing or reusable test code.

为了帮助您更好地理解这种分层在实践中的工作原理,我们现在将继续采用由外而内的方法,将旅程地图中的第一个工作流程转变为一套可执行规范。接下来,我们将讨论如何将它们发展为测试自动化系统

To help you get a better understanding of how this layering works in practice, we’ll now continue with our outside-in approach and turn the first workflow from our Journey Map into a suite of executable specifications. Next, we’ll talk about how we’d evolve them into a test automation system.

14.2.2 使用参与者链接测试自动化系统的各个层

14.2.2 Using actors to link the layers of a test automation system

Screenplay 模式根据参与者为实现其目标所需执行的任务对被测系统的预期行为进行建模,它深深嵌入到 Serenity/JS 的设计理念以及基于该框架的可执行规范和测试自动化系统中。

The Screenplay Pattern, modeling the expected behavior of the system under test in terms of tasks an actor needs to perform to achieve their goals, is deeply embedded in the design philosophy of Serenity/JS and the executable specifications and test automation systems based on this framework.

参与者代表与被测系统交互的外部用户、系统和流程。不过,从设计分层测试自动化系统的角度来看,有趣的是,参与者也是连接所有这些层的纽带——执行活动并提供抽象低级集成工具(如 Selenium WebDriver 或 HTTP 客户端)的能力。

Actors represent external users, systems, and processes interacting with the system under test. What’s interesting from the perspective of designing layered test automation systems, though, is that actors are also the link connecting all those layers—performing activities and providing abilities abstracting away the low-level integration tools like Selenium WebDriver or an HTTP client.

与 Serenity BDD 类似,在 Serenity/JS 中我们也使用 Cast 的概念来告知框架要参与哪些参与者(或者,用更专业的术语来说,我们告诉 Serenity/JS 使用哪个实现 Cast 接口的工厂类来配置参与者)。

Similar to Serenity BDD, with Serenity/JS we also use the concept of a Cast to inform the framework what actors to engage (or, in more technical terms, we tell Serenity/JS what factory class implementing the cast interface to use to configure the actors).

在只有一个参与者的实现中,或者在不需要区分不同参与者角色的实现中,我们可以为 Serenity/JS 配置一个通用角色,其中每个参与者都具有相同的能力,使他们能够以相同的方式与被测系统进行交互。如果我们将 Serenity/JS 与 Cucumber 一起使用,则可以在 Cucumber 中完成此配置带钩BeforeAll,如以下清单所示。使用BeforeAll确保我们在任何测试场景运行之前配置 Serenity/JS 参与者一次。

In implementations in which we only have one actor, or those in which we don’t need to differentiate between the different actor personas, we can configure Serenity/JS with a generic cast where every actor has the same abilities enabling them to interact with the system under test the same way. If we’re using Serenity/JS with Cucumber, this configuration can be done in Cucumber with the BeforeAll hook, as shown in the following listing. Using BeforeAll ensures that we configure Serenity/JS actors exactly once and before any of the test scenarios run.

清单 14.1 配置通用演员阵容以使用 Playwright

Listing 14.1 Configuring a generic cast of actors to use Playwright

从'@cucumber/cucumber'导入{BeforeAll,AfterAll}
从 '@serenity-js/core'导入{ configure, Cast }
从 '@serenity-js/playwright'导入{ BrowseTheWebWithPlaywright }
从‘playwright’导入*作为剧作家
 
浏览器:playwright.Browser
 
BeforeAll( async () => {                             
 
  浏览器 = 等待 playwright.chromium.launch({      
    无头:true  })
    
  配置({                                       
    演员:Cast.whereEveryoneCan( 
 
      使用Playwright.using(浏览器)浏览Web,    
  })
})
 
 
AfterAll( async () => {                              
  等待浏览器关闭()
})
import { BeforeAll, AfterAll } from '@cucumber/cucumber'
import { configure, Cast } from '@serenity-js/core'
import { BrowseTheWebWithPlaywright } from '@serenity-js/playwright'
import * as playwright from 'playwright'
 
let browser: playwright.Browser
 
BeforeAll(async () => {                            
 
  browser = await playwright.chromium.launch({     
    headless: true,
  })
    
  configure({                                      
    actors: Cast.whereEveryoneCan( 
 
      BrowseTheWebWithPlaywright.using(browser),   
    ) 
  })
})
 
 
AfterAll(async () => {                             
  await browser.close()
})

使用 BeforeAll 设置框架

Uses BeforeAll to setup the framework

实例化浏览器或其他依赖项

Instantiates browsers or other dependencies

配置 Serenity/JS

Configures Serenity/JS

使用适当的能力将参与者与集成库联系起来

Uses appropriate abilities to link actors with integration libraries

使用 AfterAll 释放所有依赖项

Uses AfterAll to release any dependencies

在清单 14.1 中,我们使用了一个名为 Playwright 的 Web 集成库。我们还依赖 Serenity/JS Playwright 集成模块来提供 Screenplay Pattern “能力” BrowseTheWebWithPlaywright。此功能充当 Playwright 的包装器,并为其 API 提供一个抽象层。

In listing 14.1, we use a web integration library called Playwright. We also rely on the Serenity/JS Playwright integration module to provide a Screenplay Pattern “ability” to BrowseTheWebWithPlaywright. This ability acts as a wrapper around Playwright and offers an abstraction layer around its APIs.

Serenity/JS 提供了多个 Web 集成模块,这些模块提供了包装流行集成工具(如 Playwright、WebdriverIO 或 Selenium WebDriver)的功能。更重要的是,它们还提供了一致的 Screenplay Pattern 样式的编程接口,使您的测试场景可以在当前和未来的 Web 集成中移植。工具。

Serenity/JS provides several web integration modules that offer abilities that wrap popular integration tools such as Playwright, WebdriverIO, or Selenium WebDriver. More importantly, they also offer a consistent Screenplay Pattern–style programming interface to allow your test scenarios to be portable across current and future web integration tools.

14.2.3 使用演员描述角色

14.2.3 Using actors to describe personas

在 Flying High Airlines 应用程序的案例中,我们希望参与者代表不同的角色,具有不同的用户详细信息和不同的身份验证凭据。这意味着Cast接口的通用实现清单 14.1 中假设所有参与者都是相同的,但这并不足够。在像我们这样稍微复杂一点的情况下,我们可以为 Serenity/JS 提供Cast接口的自定义实现,如下面的清单所示。

In the case of the Flying High Airlines app, we want our actors to represent different personas, with different user details, and different authentication credentials. This means that the generic implementation of the Cast interface you saw in listing 14.1, which assumed that all the actors are the same, will not be sufficient. In such slightly more sophisticated cases like ours we can provide Serenity/JS with a custom implementation of the Cast interface, as shown in the following listing.

清单 14.2 使用自定义配置参与者Cast

Listing 14.2 Configuring actors using a custom Cast

从'@cucumber/cucumber'导入{BeforeAll,AfterAll}
进口{ 
  演员,配置,演员表,TakeNotes,记事本
来自'@serenity-js/core'
进口{
  使用剧作家浏览网络,剧作家选项
来自'@serenity-js/playwright
 '从 '@serenity-js/rest'
导入{ CallAnApi }从‘playwright’导入*作为剧作家
 
Actors实现Cast {
 
  构造函数(
    私人只读浏览器:playwright.Browser,      
    私人只读选项:PlaywrightOptions,
  ){
  }
 
  准备(演员:演员):演员{
    返回actor.whoCan(                               
      
      使用 Playwright 浏览网页。使用(this.browser,this.options),
      
      CallAnApi.at(this.options.baseURL),
      
      TakeNotes.使用( 
        Notepad.with<TravelerNotes>({
          旅行者详细信息:TravelerDetails.of(actor.name),
        }),
      ),
    (英文):
  }
}
 
BeforeAll(() => {
  // ...为简洁起见,省略初始化浏览器
  配置({
    演员:演员(浏览器,{                      
      基本网址:'http://localhost:3000/') 
    })
  })
})
import { BeforeAll, AfterAll } from '@cucumber/cucumber'
import { 
  Actor, configure, Cast, TakeNotes, Notepad
} from '@serenity-js/core'
import {
  BrowseTheWebWithPlaywright, PlaywrightOptions
} from '@serenity-js/playwright'
import { CallAnApi } from '@serenity-js/rest'
import * as playwright from 'playwright'
 
class Actors implements Cast {
 
  constructor(
    private readonly browser: playwright.Browser,     
    private readonly options: PlaywrightOptions,
  ) {
  }
 
  prepare(actor: Actor): Actor {
    return actor.whoCan(                              
      
      BrowseTheWebWithPlaywright.using(this.browser, this.options),
      
      CallAnApi.at(this.options.baseURL),
      
      TakeNotes.using( 
        Notepad.with<TravelerNotes>({
          travelerDetails: TravelerDetails.of(actor.name),
        }),
      ),
    );
  }
}
 
BeforeAll(() => {
  // ... initializing the browser omitted for brevity
  configure({
    actors: new Actors(browser, {                     
      baseURL: 'http:/ /localhost:3000/') 
    })
  })
})

通过构造函数注入依赖项和配置

Dependencies and configuration injected via constructor

Actor 可以拥有多种能力。

Actors can have multiple abilities.

使用自定义 Actors 类,而不是通用的 Cast

Uses custom Actors class instead of the generic Cast

在清单 14.2 中,每个参与者都获得了BrowseTheWebWithPlaywright,就像清单 14.1 中的早期实现一样。除此之外,它们还能够CallAnApi由Serenity/JS REST 模块提供,它充当 HTTP 客户端的包装器,并将帮助我们在第 15 章中与 REST API 进行交互。

In listing 14.2, each actor receives an ability to BrowseTheWebWithPlaywright, just like they did in the earlier implementation from listing 14.1. Apart from that, they also receive an ability to CallAnApi, provided by the Serenity/JS REST module, which acts as a wrapper around an HTTP client and which will help us interact with REST APIs in chapter 15.

他们获得的最后一个能力是TakeNotes,它接收一个记事本,其中包含参与者将用于注册帐户或登录等活动的一些初始数据。除了读取此初始状态外,参与者还可以在测试场景中使用他们的记事本记录感兴趣的信息以供以后使用。目前,TravelerNotes清单 14.2 中记事本中保存的数据结构描述包含一个用于描述的条目TravelerDetails,如以下清单所示。

The last ability they get is one to TakeNotes, which receives a notepad that holds some initial data the actors will use for activities such as registering an account or logging in. Apart from reading this initial state, actors can use their notepads during a test scenario to record information of interest to be used later. For now, the data structure describing TravelerNotes held in the notepad from listing 14.2 contains a single entry to describe the TravelerDetails, as seen in the following listing.

清单 14.3TravelerNotes描述了参与者持有的状态对象

Listing 14.3 TravelerNotes describes the state object held by the actors

导出接口 TravelerNotes {
    旅行者详情: 旅行者详情
}
export interface TravelerNotes {
    travelerDetails: TravelerDetails
}

演员的个人详细信息(如 所述TravelerDetails)将演员与其所代表的角色联系起来。虽然可以从 JSON 或 CSV 文件甚至数据库中读取此信息,但为了演示动态生成它的模式,我们决定使用一个简单的工厂类,您可以在下面的清单中看到它。

Personal details of an actor, as described by TravelerDetails, is what associates the actor with the persona they are meant to represent. While this information could be read from a JSON or CSV file, or even a database, to demonstrate the pattern of generating it dynamically we’ve decided to use a simple factory class, which you can see in the following listing.

清单 14.4TravelerDetails生成描述每个演员的个人信息

Listing 14.4 TravelerDetails generates personal details describing each actor

导出抽象类 TravelerDetails {
  标题:字符串;  
  名字:字符串;      
  姓氏:字符串;
  电子邮件:字符串;          
  密码:字符串;
  地址:字符串;        
  国家:字符串;
  座位偏好:'靠窗' | '过道';
 
  静态(actorName:string):TravelerDetails {
    返回 {
      标题:'Mx',    
      名字:演员姓名,  
      姓氏:“旅行者”,
      电子邮件:`${ actorName }.Traveler@example.org`,
      密码:'P@ssw0rd',            
      地址:'35 Victoria Street, Alexandria',国家:'澳大利亚',
      座位偏好:‘靠窗’
    }    
  }
}
export abstract class TravelerDetails {
  title: string;  
  firstName: string;      
  lastName: string;
  email: string;          
  password: string;
  address: string;        
  country: string;
  seatPreference: 'window' | 'aisle';
 
  static of(actorName: string): TravelerDetails {
    return {
      title: 'Mx',    
      firstName: actorName,  
      lastName: 'Traveler',
      email: `${ actorName }.Traveler@example.org`,
      password: 'P@ssw0rd',            
      address: '35 Victoria Street, Alexandria', country: 'Australia',
      seatPreference: 'window'
    }    
  }
}

TypeScript 结构类型系统

TypeScript structural type system

调用静态方法TravelerDetails.of(actorName)返回一个包含诸如、等字段的普通firstName, lastNameJavaScript对象emailpassword

Invoking the static method TravelerDetails.of(actorName) returns a plain JavaScript object with fields such as firstName, lastName, email, password and so on.

值得指出的是,这个返回的对象与TravelerDetails抽象类的接口兼容,即使它没有从中继承。

It’s worth pointing out that this returned object is compatible with the interface of the TravelerDetails abstract class, even though it does not inherit from it.

这是因为 TypeScript 使用结构类型系统(http://mng.bz/m2wM),其中具有相同类型字段的对象被视为兼容,即使它们可能没有扩展相同的基类。

This works because TypeScript uses a structural type system (http://mng.bz/m2wM), where objects with the same types of fields are considered compatible even though they might not extend the same base class.

请注意,这与 Java 不同,Java 的名义类型系统a使得使用枚举之类的结构更为合适(参见第 11 章)。

Note that this is different than in Java, where its nominal type systema makes using structures like enums more appropriate (see chapter 11).


一个  Benjamin J. Evans 和 David Flanagan,《Java in a Nutshell》,第 6 版(O'Reilly Media, Inc.,2014 年),第 4 章。

a  Benjamin J. Evans and David Flanagan, Java in a Nutshell, 6th edition (O'Reilly Media, Inc., 2014), chapter 4.

14.3 在规范层捕获业务上下文

14.3 Capturing business context in the Specification layer

规范层充当测试自动化系统与其受众之间的人性化界面,负责指定被测系统的预期行为。使用 Cucumber.js 之类的工具,我们可以轻松地使用业务词汇来表达场景,并用其他上下文来丰富它们。这通常包括功能背后的动机,甚至包括指向外部文档、图表等的超链接。

The Specification layer acts as a human-friendly interface between the test automation system and its audience responsible for specifying the intended behavior of the system under test. Using a tool like Cucumber.js makes it easy for us to express the scenarios using business vocabulary and enrich them with additional context. This typically includes the motivation behind a feature, or even hyperlinks to external documentation, diagrams, and so forth.

此外,Cucumber 的语言 Gherkin 不仅支持用英语表达测试场景,还支持用 70 多种其他语言(https://cucumber.io/docs/gherkin/languages/)表达测试场景,包括西班牙语、法语、德语、日语或阿拉伯语。这是一个需要记住的重要功能,因为它可以大大提高我们的场景对非英语母语人士的可访问性,并有助于让他们保持参与度。

Additionally, Gherkin, the language of Cucumber, supports expressing test scenarios not just in English, but also in over 70 other languages (https://cucumber.io/docs/gherkin/languages/), including Spanish, French, German, Japanese, or Arabic. This is an important feature to remember, as it can greatly improve the accessibility of our scenarios to non-native English speakers and help to keep them engaged.

除了可访问性之外,另一个可以大大提高业务受众参与场景创建和审查过程可能性的重要因素是场景的可理解性。由于我们的目标受众通常只有非常有限的时间来审查和提供测试场景的反馈,因此确保这些场景简洁并仅突出直接影响结果的重要步骤非常重要。

Apart from accessibility, another important factor that greatly improves the likelihood of business audiences engaging in the process of creating and reviewing the scenarios is their comprehensibility. Since our target audience typically has a very limited amount of time to dedicate to reviewing and providing feedback on our test scenarios, it’s important to ensure those scenarios are succinct and highlight only the important steps directly affecting the outcome.

帮助我们创建此类场景的一个好方法是确保它们的步骤与旅程地图中的高级任务相对应。例如,我们讨论的快乐路径注册工作流可以使用只有两个步骤的场景来表达,如清单 14.5 所示,并对应于注册和登录的两个高级任务,如图 14.12 所示。

A great way to help us create such scenarios is to ensure that their steps correspond to the high-level tasks from the Journey Map. For example, the happy path sign-up workflow we discussed could be expressed using a scenario with only two steps, as seen in listing 14.5, and corresponding to the two high-level tasks to sign up and sign in, as depicted in figure 14.12.

清单 14.5 使用有效旅行者详细信息注册的正面场景

Listing 14.5 Positive scenario of signing up using valid traveler details

功能:注册
 
  乘客必须注册常旅客计划才能预订航班
  从而为他们赢得飞行常客积分。
 
  规则:必须注册飞行常客帐户才能使用该系统
 
    场景:使用有效的旅行者详细信息进行注册
 
      Tracy 使用有效的旅行者详细信息进行注册时
      然后她应该可以登录
Feature: Sign up
 
  Customers must sign up to the Frequent Flyer program to book flights
  that earn them Frequent Flyer points.
 
  Rule: Registered Frequent Flyer account is required to use the system
 
    Scenario: Sign up using valid traveler details
 
      When Tracy signs up using valid traveler details
      Then she should be able to sign in

图 14.10 中提到的负面测试场景,即旅行者尝试使用已经注册的电子邮件地址进行注册,最多可以用四个步骤来表达,如下面的清单所示。

The negative test scenario, mentioned in figure 14.10, where the traveler tries to sign up using an email address that’s already been registered could be expressed using at most four steps, as seen in the following listing.

清单 14.6 使用重复电子邮件地址注册的负面场景

Listing 14.6 Negative scenario of signing up using duplicate email address

功能:注册
 
  # ...
  
  规则:不允许使用重复的用户名
 
    场景:使用重复的电子邮件地址注册
 
      迈克·史密斯 (Mike Smith) 是现有的飞行常客会员。
      他的妻子珍妮·史密斯 (Jenny Smith) 没有飞行常客账户。
 
      假设Mike 已使用以下详细信息进行注册:
        | 电子邮件 | smiths@example.org |
       Jenny 尝试使用以下方式注册时:
        | 电子邮件 | smiths@example.org |
       然后她应该被告知一个错误:“电子邮件已存在”
        她应该有一个重置密码的选项
Feature: Sign up
 
  # ...
  
  Rule: Duplicate usernames are not allowed
 
    Scenario: Sign up using duplicate email address
 
      Mike Smith is an existing Frequent Flyer member.
      His wife Jenny Smith does not have a Frequent Flyer account.
 
      Given Mike has signed up using the following details:
        | email | smiths@example.org |
       When Jenny tries to sign up using:
        | email | smiths@example.org |
       Then she should be advised of an error: "Email exists"
        And she should be presented with an option to reset password

请注意,清单 14.5 和 14.6 中的场景仅突出显示了影响最终结果的重要步骤,并使用场景描述和规则名称形式的元数据来提供额外的上下文。

Note that both the scenarios from listings 14.5 and 14.6 highlight only the important steps that affect the final outcome and use metadata in the form of scenario descriptions and rule names to provide additional context.

在底层,Cucumber 功能文件中捕获的每个高级步骤都对应着我们旅程地图中的一个或最多几个高级任务,每个任务都由一个 Screenplay 模式任务表示。让我们看看 Cucumber 步骤和 Screenplay 任务之间的映射是如何完成的。

Under the hood, each of the high-level steps captured in a Cucumber feature file corresponds to one or at most a handful of high-level tasks from our Journey Map, each represented by a Screenplay Pattern task. Let’s look into how this mapping between Cucumber steps and Screenplay tasks is done.

与 Cucumber JVM 类似,Cucumber.js 也使用步骤定义的概念将场景步骤(例如“当 Tracy 使用有效的旅行者详细信息进行注册时”)映射到执行与被测系统必要交互的可执行代码。

Similar to Cucumber JVM, Cucumber.js also uses the concept of step definitions to map a scenario step such as “When Tracy signs up using valid traveler details” to executable code that performs the necessary interactions with the system under test.

清单 14.7 使用 Cucumber.js 实现步骤定义的示例

Listing 14.7 Example step definition implementation with Cucumber.js

// “当 Tracy 使用有效的旅行者详细信息注册时”
(‘{actor} 使用有效的旅行者详细信息注册’时,(actor:Actor)=>     
  演员.尝试(                                                         
    注册.使用(                                                           
      注释 <TravelerNotes>().get('travelerDetails')                         
    ),
    VerifySubmission.succeededWith('注册成功')
  ));
// "When Tracy signs up using valid traveler details"
When('{actor} signs up using valid traveler details', (actor: Actor) =>     
  actor.attemptsTo(                                                         
    SignUp.using(                                                           
      notes<TravelerNotes>().get('travelerDetails')                         
    ),
    VerifySubmission.succeededWith('registered successfully')
  ));

虽然清单 14.7 中给出的步骤定义简短而简洁,但其中有很多内容,所以让我们逐行检查一下:

Even though the step definition presented in listing 14.7 is short and concise, there’s quite a lot going on there, so let’s go through it line by line:

  1. 我们使用 Cucumber 表达式 ( https://github.com/cucumber/cucumber-expressions ) 来定义 Cucumber 应使用的模式,以便将场景步骤与我们的步骤定义进行匹配。Cucumber 还将替换{actor}token,称为 Cucumber 表达式参数,由于自定义 Cucumber 表达式参数类型定义,它与 Serenity/JS 参与者相关联。

  2. We use a Cucumber expression (https://github.com/cucumber/cucumber-expressions) to define a pattern Cucumber should use to match a scenario step with our step definition. Cucumber will also substitute the {actor} token, called a Cucumber expression parameter, with a Serenity/JS actor thanks to a custom Cucumber expression parameter type definition.

  3. 接下来我们使用actor.attemptsTo(...activities: Activity[])API在 Cucumber 步骤和一系列 Screenplay Pattern 任务之间创建映射。这会告知参与者应执行哪些活动来完成该步骤。

  4. Next, we use the actor.attemptsTo(...activities: Activity[]) API to create a mapping between a Cucumber step and a sequence of Screenplay Pattern tasks. This informs the actor what activities it should perform to accomplish the step.

  5. 我们SignUp通过其静态工厂方法实例化一个自定义任务SignUp.using,我们将在第 15 章中详细探讨。

  6. We instantiate a custom task to SignUp via its static factory method SignUp.using, which we’ll investigate in detail in chapter 15.

  7. SignUp我们用注释将任务参数化为travelerDetails。此注释从参与者的记事本中检索执行注册任务所需的数据;您在清单 14.2 中第一次遇到的构造。

  8. We parameterize the task to SignUp with a note on travelerDetails. This note retrieves the data required to perform the task to sign up from the actor’s notepad; a construct you first encountered in listing 14.2.

  9. 最后,我们提供另一个自定义任务来验证我们的表单提交是否成功。

  10. Finally, we provide another custom task to verify if our form submission was successful.

清单 14.7 中发生的转换如图 14.16 所示。

Transformations that take place in listing 14.7 are depicted in figure 14.16.

图 14.16 场景步骤被转换并映射到剧本模式任务。

Figure 14.16 A scenario step is transformed and mapped to Screenplay Pattern tasks.

Cucumber 参数类型和 Serenity/JS 参与者

Cucumber parameter types and Serenity/JS actors

默认情况下,Cucumber.js 无法解释{actor}{pronoun}代币在我们的步骤定义中,因为它们不属于其标准词汇表。为了扩展它,我们需要定义自定义参数类型(http://mng.bz/5meD):

By default, Cucumber.js will not be able to interpret the {actor} and {pronoun} tokens in our step definition since they’re not part of its standard vocabulary. To expand it, we need to define custom parameter types (http://mng.bz/5meD):

从'@cucumber/cucumber'导入{defineParameterType}
从 '@serenity-js/core' 导入 { actorCalled, actorInTheSpotlight }
 
定义参数类型({
    名称:“演员”,
    正则表达式:/[AZ][az]+/,
    变压器:(名称:字符串)=> actorCalled(名称)
})
定义参数类型({
    名称:'代词',
    正则表达式:/他|她|他们|他的|她的|他们的/,
    变换器:()=> actorInTheSpotlight()    
})
import { defineParameterType } from '@cucumber/cucumber'
import { actorCalled, actorInTheSpotlight } from '@serenity-js/core'
 
defineParameterType({
    name: 'actor',
    regexp: /[A-Z][a-z]+/,
    transformer: (name: string) => actorCalled(name)
})
defineParameterType({
    name: 'pronoun',
    regexp: /he|she|they|his|her|their/,
    transformer: () => actorInTheSpotlight()    
})

自定义参数类型{actor}使 Cucumber.js 调用 Serenity/JS APIactorCalled(name).此方法检索已实例化的参与者或实例化一个参与者并将其传递给我们的Cast接口实现,其中演员配置了能力和初始状态Notepad。另一个自定义参数类型,{pronoun},只是检索最近访问的演员。

The custom parameter type {actor} makes Cucumber.js invoke the Serenity/JS API actorCalled(name). This method retrieves the actor that’s already been instantiated or instantiates one and passes it to our implementation of the Cast interface, where the actor is configured with abilities and the initial state of the Notepad. The other custom parameter type ,{pronoun}, simply retrieves the most recently accessed actor.

请注意,正则表达式适用于用英语表达的场景,需要进行调整以支持您可能想要与受众使用的其他语言。

Note that the regular expressions work for scenarios expressed in English and will need to be adjusted to support other languages you might want to use with your audience.

我们以类似的方式定义步骤“她应该能够登录”,这次依靠 Cucumber 将令牌替换{pronoun}为最近访问的参与者,如下所示。

We define the step “she should be able to sign in” in a similar manner, this time relying on Cucumber to replace the token {pronoun} with the most recently accessed actor, as follows.

清单 14.8 使用代词来指代最近使用的演员

Listing 14.8 Using pronouns to reference most recently used actor

Then('{pronoun} 应该能够登录', async (actor: Actor) => {
    const details = notes <TravelerNotes>().get('travelerDetails') 复制代码
 
    等待actor.attemptsTo(
        登录.使用(电子邮件详细信息,密码详细信息),
})
Then('{pronoun} should be able to sign in', async (actor: Actor) => {
    const details = notes<TravelerNotes>().get('travelerDetails')
 
    await actor.attemptsTo(
        SignIn.using(details.email, details.password),
    )
})

因为更自然地认为该任务SignIn只需要两个参数,即电子邮件和密码,所以在清单 14.8 中我们提取了一个变量,travelerDetails,以避免代码重复,然后将所需的参数单独传递给任务。

Since it’s more natural to think of the task to SignIn as only requiring two arguments, the email and the password, in listing 14.8 we extract a variable, travelerDetails, to avoid code duplication and then pass the required parameters individually to the task.

您可能已经注意到,高级任务SignUpSignIn尚未实现。事实上,在使用 Serenity/JS 时,设计任何此类自定义、特定领域任务的方法与我们设计其余测试自动化系统的方法完全相同,这是一种常见做法。我们从外部开始,优化每一层的界面,以提升目标受众的体验。就像我们优化 Cucumber 场景以提升商业受众的阅读体验一样,我们优化自定义 Serenity/JS 任务以提升阅读体验并减少技术受众的认知负荷。

You might have noticed that the high-level tasks to SignUp and SignIn haven’t been implemented yet. It is, in fact, a common practice when working with Serenity/JS to approach designing any such custom, domain-specific tasks the exact same way we approach designing the rest of our test automation system. We start from the outside and optimize the interfaces in each layer for the experience of its intended audience. And just like we optimize the Cucumber scenarios for the reading experience of the business audience, we optimize our custom Serenity/JS tasks for the reading experience and to reduce the cognitive load of the technical audience.

这种一致的由外而内的方法在我们测试自动化系统的所有层中重复使用,不仅使我们的代码更易于阅读、理解和维护;它还有助​​于提高其内聚性并减少耦合,并使我们的代码更易于重用。当我们讨论在第 15 章。

This consistent outside-in approach repeated throughout all the layers of our test automation system makes our code not only easier to read, comprehend, and maintain; it also helps to improve its cohesion and reduce coupling and make our code much easier to reuse. We’ll talk about it more when we discuss implementing the Domain layer in chapter 15.

异步步骤定义的实现模式

Implementation patterns for asynchronous step definitions

Serenity/JS 充分利用了 JavaScript 的异步特性,因此框架提供的所有 API 默认都是异步的。

Serenity/JS fully embraces the asynchronous nature of JavaScript, and thus all the APIs provided by the framework are asynchronous by default.

这在实践中意味着actor.attemptsTo(...)API返回一个Promisehttp://mng.bz/69p6)。这Promise需要返回给调用 Serenity/JS 代码的任何测试运行器(在本例中为 Cucumber.js),以便它可以正确同步其执行。

What this means in practice is that the actor.attemptsTo(...) API returns a Promise (http://mng.bz/69p6). This Promise needs to be returned to any test runner that invokes Serenity/JS code, in this case Cucumber.js, so that it can correctly synchronize its execution.

根据给定步骤定义的复杂性,您可以在此处应用两种主要步骤定义实现模式。

There are two main step definition implementation patterns you can apply here, depending on the complexity of a given step definition.

第一种模式涉及使用箭头函数表达式(http://mng.bz/o5wv)并且适用于只需要调用actor .attemptsTo(...)方法的简单步骤定义,例如:

The first pattern involves using an arrow function expression (http://mng.bz/o5wv) and is applicable with simple step definitions that require invoking just the actor .attemptsTo(...) method, for example:

Given('{actor} 已报名', (actor: Actor) =>
  actor.attemptsTo(/* 活动 */) 
Given('{actor} has signed up', (actor: Actor) =>
  actor.attemptsTo(/* activities */) 
)

这个最小的无括号箭头函数表达式帮助我们避免了等效但更加冗长的传统匿名函数的语法噪音:

This minimal bracket-less arrow function expression helps us avoid the syntax noise of the equivalent but much more verbose traditional anonymous function:

Given('{actor} 已报名', function (actor: Actor): Promise<void> {
  返回 actor.attemptsTo(/* 活动 */)
})
Given('{actor} has signed up', function (actor: Actor): Promise<void> {
  return actor.attemptsTo(/* activities */)
})

第二种模式涉及使用async/awaithttp://mng.bz/new4)语法,并且更适用于具有多个参与者或引入其他变量的更复杂的步骤定义:

The second pattern involves using the async/await (http://mng.bz/new4) syntax and works better for the more complex step definitions with multiple actors or introducing additional variables:

// 当 Alice 向 Bob 发送消息“你好!”
当(‘{actor} 向 {actor} 发送消息 {string}’时, 
  异步(发送者:演员,消息文本:字符串,接收者:演员)=> {
    等待发送者.尝试(
      发送消息.使用(消息文本).到(接收者.姓名)
    等待接收者.尝试(
      确保(Messages.received(),包含(messageText)),
  } 
// When Alice sends a message "hello!" to Bob
When('{actor} sends a message {string} to {actor}', 
  async (sender: Actor, messageText: string, receiver: Actor) => {
    await sender.attemptsTo(
      SendMessage.with(messageText).to(receiver.name)
    )    
    await receiver.attemptsTo(
      Ensure.that(Messages.received(), contain(messageText)),
    )
  } 
)

第二种模式要求函数使用async关键字,必须有花括号,并且函数体中的任何异步调用都必须使用await关键字

This second pattern requires the function to use the async keyword, the curly brackets to be present, and any asynchronous calls in the function body to use the await keyword.

概括

Summary

  • 旅程图是一种可视化技术,有助于创建系统支持的工作流的心理模型,并将其与外部参与者及其目标联系起来。

  • Journey Mapping is a visual technique that helps to create a mental model of workflows supported by the system and associate them with external actors and their goals.

  • 旅程图可用于识别要使用自动化测试执行的候选工作流程,并影响首先进行哪些工作。

  • Journey Mapping can be used to identify candidate workflows to be exercised using automated tests and influence which ones to work on first.

  • 规范层是测试自动化系统的顶层。它负责捕获系统利益相关者认为重要的工作流程、业务规则和使用示例,并应以利益相关者能够理解且易于阅读的语言来表达。

  • Specification layer is the top layer of a test automation system. It is responsible for capturing the workflows, business rules, and usage examples that the stakeholders of the system find important and should be expressed in a language the stakeholders can understand and are comfortable with reading.

  • 领域层是测试自动化系统的中间层。它模拟完成规范层中表达的工作流所需的确切活动。基于 Serenity/JS 的测试自动化系统使用 Screenplay Pattern API 来模拟参与者执行的任务。

  • The Domain layer is the middle layer of a test automation system. It models the exact activities required to complete the workflows expressed in the Specification layer. Test automation systems based on Serenity/JS use Screenplay Pattern APIs to model the tasks performed by the actors.

  • 集成层是测试自动化系统的底层。它弥合了业务领域和技术领域之间的鸿沟。它将领域层中表达的参与者活动转换为与被测系统特定外部接口的精确低级交互,并管理这些接口的低级客户端,如 HTTP 客户端或 Web 浏览器驱动程序。

  • The Integration layer is the bottom layer of a test automation system. It bridges the gap between the business domain and the technology domain. It translates actors’ activities expressed in the Domain layer to exact low-level interactions with the specific external interfaces of the system under test and manages the low-level clients of those interfaces, like an HTTP client or a web browser driver.

  • 端到端测试场景执行完整的工作流程,但不必针对完全组装和部署的系统运行。

  • An end-to-end test scenario performs a full workflow but doesn’t have to run against a fully assembled and deployed system.

  • 按规则拆分有助于我们避免大量的测试自动化工作,并允许我们一次自动执行一个业务规则的场景。

  • Splitting by rule helps us to avoid big batches of test automation work and allows us to automate scenarios one business rule at a time.

  • Serenity/JS Screenplay Pattern 任务可以参数化,以实现工作流程的变化。

  • Serenity/JS Screenplay Pattern tasks can be parameterized to enable variations in workflows.

  • 演员使用和管理场景相关数据的Notepad能力。TakeNotes

  • Actors use Notepad and the ability to TakeNotes to manage scenario-related data.

  • 所有 Serenity/JS API 都与原生 JavaScript Promises 兼容,并为该语言的异步特性提供一流的支持。

  • All Serenity/JS APIs are compatible with native JavaScript Promises and offer a first-class support for the asynchronous nature of the language.

  • 使用本章存储库中的测试自动化系统进行实验,以学习更多的。

  • Experiment with the test automation system in this chapter’s repository to learn more.

15 使用 Serenity/JS 实现便携式测试自动化

15 Portable test automation with Serenity/JS

本章封面

This chapter covers

  • 设计测试自动化系统的领域层
  • Designing the Domain layer of a test automation system
  • 支持可移植测试自动化的设计模式
  • Design patterns supporting portable test automation
  • 利用混合测试实现非 UI 交互
  • Leveraging non-UI interactions with blended testing
  • 测试代码封装和重用模式
  • Test code encapsulation and reuse patterns

在第 14 章中,您了解了遵循分层架构模式如何帮助您设计可扩展的测试自动化系统。您还了解了引入规范层如何帮助您捕获有关业务环境及其规则、工作流和场景的信息,以及与系统交互的参与者及其试图实现的目标。

In chapter 14, you learned how following the layered architecture pattern can support you in designing scalable test automation systems. You’ve also seen how introducing a Specification layer can help you capture information about the business context and its rules, workflows, and scenarios, as well as the actors interacting with the system and the goals they’re trying to accomplish.

在本章中,我们将继续探讨对测试自动化系统架构进行分层的想法。我们将研究如何在测试自动化代码中反映业务领域的概念和词汇。我们还将研究有助于将实施重点放在业务流程建模和抽象较低级别测试集成工具上的模式。

In this chapter, we’ll continue to explore the idea of layering the architecture of our test automation system. We’ll investigate ways to reflect the concepts and vocabulary from the business domain in our test automation code. We’ll also study patterns that help to focus our implementation on modeling the business process and abstracting the lower-level test integration tools.

在我们的探索中,我们将确定 Serenity/JS API,以帮助使我们的测试代码可移植。您还将看到,在设计时考虑可移植性如何有助于创建可重复使用的测试代码,这些代码可以在不同的环境中使用,并且可以扩展,不仅支持项目中的多个测试套件,还支持组织中的多个团队。

In our exploration, we’ll identify Serenity/JS APIs to help to make our test code portable. You’ll also see how designing with portability in mind helps to create reusable test code that can be used in different contexts and scale to support not only multiple test suites within your project, but also multiple teams across your organization.

如果您想尝试我们将在本章中探讨的示例测试自动化系统,您可以在本书的 GitHub 存储库中找到它,也可以从 Manning 网站下载它。

If you’d like to experiment with the example test automation system we’ll be exploring in this chapter, you can find it in this book’s GitHub repository or download it from the Manning website.

15.1 设计测试自动化系统的领域层

15.1 Designing the Domain layer of a test automation system

一个典型的企业系统负责表示业务领域的概念:有关业务情况的信息。但更重要的是,它负责确保通过其外部接口与其交互的任何外部参与者都按照其业务规则进行交互并遵循支持的用例。例如,航班预订系统将对航班、航班时刻表或预订等业务领域概念进行建模。它将存储有关业务情况的信息,例如当前有哪些航班可用及其详细信息。它还将确保任何外部参与者(例如旅行者)不会预订过去日期的机票或不存在的航班,因为这违反了其业务规则。

A typical enterprise system is responsible for representing concepts of the business domain: information about the business situation. More importantly, however, it is responsible for ensuring that any external actors interacting with it through its external interfaces do so in accordance with its business rules and follow the supported use cases. For example, a flight booking system would model business domain concepts such as a flight, flight schedule, or booking. It would store information about the business situation, such as what flights are currently available, and their details. It would also ensure that any external actors, such as the travelers, don’t book tickets for dates in the past or flights that don’t exist, which is something that would be against its business rules.

另一方面,测试自动化系统的目的是帮助引导被测系统发展到这个阶段。它还旨在验证其实际行为的正确性,并确保遵守其需要遵守的业务规则。

The purpose of a test automation system, on the other hand, is to help guide the evolution of the system under test to this stage. It is also to verify the correctness of its actual behavior and ensure that the business rules it needs to obey are adhered to.

为了有效地做到这一点,测试自动化系统必须与被测系统交互,模拟外部参与者的活动并验证被测系统的响应方式。此类交互通常通过被测系统公开的外部接口实现,例如 Web UI、移动应用程序或编程 API(如 REST)。但是,将外部接口本身作为验收测试的主题是错误的(将会有其他测试)。相反,外部接口是测试自动化系统与被测系统实现的业​​务逻辑交互的手段 - 通过这些门户,测试可以与业务领域的概念进行交互。请注意,这与实际的外部参与者如何看待我们的企业系统接口类似:旅行者使用 Web UI 的原因是为了预订机票,而不是为了与 UI 交互而与 UI 交互。

To do that effectively, a test automation system must interact with the system under test, simulating the activities of the external actors and verifying how the system under test responds. Such interactions are typically achieved through the external interfaces exposed by the system under test, such as a Web UI, a mobile app, or programmatic APIs, such as REST. However, it is a mistake to focus on the external interfaces themselves as the subjects of our acceptance tests (there will be other tests for that). Rather, external interfaces are the means for a test automation system to interact with the business logic implemented by the system under test—doors through which the tests can interact with concepts from the business domain. Note that this is similar to how the actual external actors perceive the interfaces of our enterprise systems: the reason for a traveler to use a Web UI is to book a plane ticket, not to interact with the UI for the sake of it.

由于测试自动化系统是为了模拟外部参与者的活动而开发的,因此其领域层专注于对这些活动进行建模,并将它们与业务领域含义联系起来。就像在现实世界中一样,我们不会根据毫无意义的点击序列来推断实际旅行者所执行的活动;我们的验收测试也不应该如此。真正的旅行者会搜索航班、调整出发日期、填写登记表、选择最便宜的航班等等。

Since a test automation system is developed to simulate the activities of external actors, its Domain layer focuses on modeling those activities and associating them with business domain meaning. Just like in the real world, we don’t reason about the activities performed by an actual traveler in terms of sequences of meaningless clicks; neither should our acceptance tests. A real traveler would search for a flight, adjust a departure date, fill out the registration form, pick the cheapest flight, and so on.

当然,这些较高级别的活动是由较低级别的活动组成的,而较低级别的活动又由更低级别的活动组成,一直到最低级别的、特定于界面的交互,例如单击按钮、在表单字段中输入值或发送 HTTP 请求。然而,这些低级活动是实现细节,是更重要的“什么”的“如何”和更重要的“为什么”的“如何”

Of course, those higher-level activities are composed of lower-level activities, which are composed of even lower-level activities, all the way to the lowest-level, interface-specific interactions like clicking on a button, entering a value into a form field, or sending an HTTP request. Nevertheless, those low-level activities are an implementation detail, the how of the much more important what, and the even more important why.

15.1.1 建模业务领域任务

15.1.1 Modeling business domain tasks

为了在为什么、什么和如何之间实现平稳过渡,我们需要逐步将高级任务分解为低级任务,一次分解一层抽象。考虑我们已经在第 14 章的旅程地图上可视化的注册任务(图 14.12),并在图 15.1 中重复了该任务并添加了更多细节。这个高级任务的目标(或为什么)是让参与者注册。参与者需要做什么来实现该目标由三个子任务表示:找到注册表,填写并提交。成功完成这三个子任务意味着成功完成主要任务,并导致创建用户帐户并允许参与者登录。相反,未能成功完成这三个子任务将导致无法创建用户帐户,不允许参与者登录,并提供有关如何更正他们在注册表中提供的信息的建议。前者结果在图 15.1 中标记为积极情景后果,后者标记为消极情景后果。

To implement a smooth transition between the why, the what, and the how, we need to gradually decompose the higher-level tasks into lower-level ones, one layer of abstraction at a time. Consider the task to sign up that we already visualized on our journey map in chapter 14 (figure 14.12) and repeated here with additional details in figure 15.1. The goal, or the why, of this high-level task is for the actor to sign up. What the actor needs to do to accomplish that goal is represented by the three subtasks: locate the registration form, fill it out, and submit it. Successful completion of these three subtasks implies a successful completion of the main task and results in the user account created and the actor allowed to sign in. Conversely, a failure to complete the three subtasks successfully results in the user account not getting created, the actor not being allowed to sign in, and advice given on how to correct the information they provided in their registration form. The former outcome is marked in figure 15.1 as positive scenario consequences, and the latter as negative scenario consequences.

图 15.1 通过 Web UI 注册的任务允许旅行者在成功完成所有三个子任务后继续登录工作流程。根据旅行者提供的详细信息,执行任务会导致积极或消极的场景后果。

Figure 15.1 The task to sign up via Web UI allows the traveler to proceed to the sign-in workflow whenall three subtasks are completed successfully. Depending on the traveler details provided, performing the task leads to positive or negative scenario consequences.

请注意,如图 15.1 所示,影响报名任务结果的唯一因素是参与者提供的旅行者详细信息。如果参与者提供有效的旅行者详细信息,则任务成功,如果他们提供无效详细信息,则任务失败。

Note that as presented in figure 15.1, the only factor that affects the outcome of the task to sign up is what traveler details the actor provides. If the actor provides valid traveler details the task is successful, and if they provide invalid details the task results in a failure.

如果我们使用函数式编程类比,将注册任务视为一个函数,旅客详细信息是一个参数这种函数,而有效或无效旅行者详细信息的具体示例是函数参数。当我们在下一节讨论如何实现 Serenity/JS Screenplay Pattern 任务时,这种区别将会变得有用部分。

If we were to use a functional programming analogy and think of the task to sign up as a function, traveler details are a parameter of such function, while a concrete example of valid or invalid traveler details is a function argument. This distinction will become useful when we discuss ways to implement Serenity/JS Screenplay Pattern tasks in the next section.

15.1.2 实施业务领域任务

15.1.2 Implementing business domain tasks

喜欢使用 Serenity BDD,定义自定义 Serenity/JS Screenplay Pattern 任务的最简单方法是使用Task.whereAPI。它具有以下签名:

Like with Serenity BDD, the easiest way to define a custom Serenity/JS Screenplay Pattern task is to use the Task.where API. It has the following signature:

Task.where(说明:字符串,...活动:Activity[]):任务
Task.where(description: string, ...activities: Activity[]): Task

Serenity/JS Screenplay Pattern 任务本质上是子任务或交互的集合,此方法具有两个有趣的属性,可以帮助我们创建这样的聚合:

Serenity/JS Screenplay Pattern tasks are essentially aggregates of subtasks or interactions, and this method has two interesting properties that help us create such aggregates:

  • 它使用 rest 参数语法(请参阅 MDN“Rest 参数”:http://mng.bz/neK2)来接受一个或多个“活动”。这使得从子任务或交互中快速组合任务变得容易。

  • It uses the rest parameter syntax (see MDN “Rest Parameters”: http://mng.bz/neK2) to accept one or more “activities.” This makes it easy to quickly compose tasks from subtasks or interactions.

  • 如果省略活动,该方法将生成一个Task对象当演员执行时会抛出ImplementationPendingError,而 Serenity/JS 会将发生此类错误的场景报告为待执行。这是一种定义我们尚不确定如何实现的空任务的简单方法,并获取测试执行报告,指出它们在我们的测试场景中的位置。

  • If activities are omitted, the method produces a Task object which upon execution by an actor throws an ImplementationPendingError, and Serenity/JS reports scenarios where such an error occurred as pending implementation. This is a simple way to define empty tasks that we’re not yet sure how to implement and get a test execution report pointing out where they are in our test scenarios.

在清单 15.1 中,您可以看到这两个属性如何帮助我们实现注册任务及其三个子任务,如图 15.1 所示。

In listing 15.1, you can see how those two properties can help us implement a task to sign up and its three subtasks, as depicted in figure 15.1.

清单 15.1 实现 SignUp 的待处理任务

Listing 15.1 Implementing a pending task to SignUp

从 '@serenity-js/core' 导入 { Task }
 
const LocateRegistrationForm = () =>
    Task.where('#actor 定位登记表')
 
const FillOutRegistrationForm = (旅行者详细信息:旅行者详细信息) =>
    Task.where('#actor填写登记表')
 
const SubmitRegistrationForm = () =>
    Task.where('#actor 提交登记表')
 
const SignUp = (旅行者详细信息:旅行者详细信息) =>
    Task.where('#actor 注册',
        找到注册表格(),
        填写登记表(旅客详细信息),
        提交注册表单(),
import { Task } from '@serenity-js/core'
 
const LocateRegistrationForm = () =>
    Task.where('#actor locates registration form')
 
const FillOutRegistrationForm = (travelerDetails: TravelerDetails) =>
    Task.where('#actor fills out registration form')
 
const SubmitRegistrationForm = () =>
    Task.where('#actor submits registration form')
 
const SignUp = (travelerDetails: TravelerDetails) =>
    Task.where('#actor signs up',
        LocateRegistrationForm(),
        FillOutRegistrationForm(travelerDetails),
        SubmitRegistrationForm(),
    )

在清单 15.1 中,我们使用箭头函数表达式(这是您在第 14.3 节中第一次遇到的一种构造)来定义生成任务的函数,这些任务用于查找注册表、填写并提交。所有这些函数在定义中都省略了子任务列表,这意味着它们将被报告为待执行。我们还定义了一个生成注册任务的函数,它接受旅行者详细信息的任务参数(清单 14.4 中详细介绍)。

In listing 15.1, we use arrow function expressions, a construct you first encountered in section 14.3, to define functions producing tasks to locate registration form, fill it out, and submit it. All of which omit the list of their sub-tasks in their definition, which means they’ll get reported as pending implementation. We’ve also defined a function producing a task to sign up, which accepts a task parameter of traveler details (presented in detail in listing 14.4).

尽管我们的任务还没有做任何有意义的事情,但它们已经帮助我们捕捉业务领域的概念和词汇,例如旅行者详细信息或注册。这种使用业务领域词汇来命名任务的模式有助于建立业务人员和技术人员共享的一致术语和通用语言。1

Even though our tasks don’t yet do anything meaningful, they already help us to capture business domain concepts and vocabulary, such as traveler details or signing up. This pattern of using business domain vocabulary to name tasks helps to establish consistent terminology and ubiquitous language shared by both the business and technology folk.1

此外,即使有了这个基本的实现,我们也可以继续采用由外而内的方法来实现我们的测试自动化系统。具体来说,我们已经可以将清单 15.1 中的任务插入到我们的 Cucumber 步骤定义中:

Also, having even this rudimentary implementation in place allows us to continue with the outside-in approach to implementing our test automation system. In particular, we can already plug our task from listing 15.1 into our Cucumber step definition:

当(‘{actor} 使用以下详细信息注册:’,
    (演员:演员,数据:数据表)=>
        演员.尝试(
            注册(data.rowsHash()),
(英文):
When('{actor} signs up using following details:',
    (actor: Actor, data: DataTable) =>
        actor.attemptsTo(
            SignUp(data.rowsHash()),
        )
);

在这个抽象层次上,我们尽量避免在任务名称中使用任何特定于集成接口的词汇,甚至清单 15.1 中的注册任务的实现对于技术和非技术受众来说仍然是完全可以理解的。下一步是让我们更深入地研究抽象层次,并研究如何让任务在特定条件下与系统进行实际交互。测试。

At this level of abstraction, we try to avoid using any vocabulary specific to the integration interface in task names, and even the implementation of the task to sign up from listing 15.1 is still perfectly understandable to both the technology and nontechnology audience. The next step is for us to dive one level of abstraction deeper and investigate the how—a way to make the tasks perform the actual interactions with the system under test.

15.1.3 将交互组合成任务

15.1.3 Composing interactions into tasks

相似的对于 Serenity BDD,Serenity/JS 任务要为被测系统提供刺激,需要委托给交互。Serenity/JS 交互是低级活动,直接调用参与者能力的方法。然后,这些方法调用低级、外部接口特定的集成工具,例如 Web 浏览器驱动程序或 HTTP 客户端。

Similar to Serenity BDD, for a Serenity/JS task to provide stimuli to the system under test it needs to delegate to an interaction. Serenity/JS interactions are low-level activities that directly invoke methods on actors’ abilities. Those methods then invoke the low-level, external interface-specific integration tools, such as a Web browser driver or an HTTP client.

Serenity/JS 集成模块,例如@serenity-js/webhttps://www.npmjs.com/package/@serenity-js/web)和@serenity-js/rest( https://www.npmjs.com/package/@serenity-js/rest ) 提供了数十种此类低级交互,在本节中,您将了解如何将它们组合成有意义的任务。以下代码显示了查找注册表单任务的示例实现,该任务由一组交互组成,用于导航到主页并单击“注册”按钮。

Serenity/JS integration modules, like @serenity-js/web (https://www.npmjs.com/package/@serenity-js/web) and @serenity-js/rest (https://www.npmjs.com/package/@serenity-js/rest) provide dozens of such low-level interactions, and in this section, you’ll see how to compose them into meaningful tasks. The following code shows an example implementation of the task to locate the registration form, as expressed by a composition of interactions to navigate to the home page and click on a button that says Register.

清单 15.2 将内置 Web 交互组合成任务

Listing 15.2 Composing built-in Web interactions into tasks

从 '@serenity-js/core' 导入 { Task }
从 '@serenity-js/web' 导入 { 点击,导航 }
 
const LocateRegistrationForm = () =>
    Task.where(`#actor 定位注册表单`,
        导航至('/'),
        Click.on(Form.buttonCalled('注册')),
import { Task } from '@serenity-js/core'
import { Click, Navigate } from '@serenity-js/web'
 
const LocateRegistrationForm = () =>
    Task.where(`#actor locates the registration form`,
        Navigate.to('/'),
        Click.on(Form.buttonCalled('Register')),
    )

请注意,在清单 15.2 中,单击和导航的交互来自 Serenity/JS Web 模块。此模块与所使用的低级集成工具无关。更具体地说,它提供了一个一致的抽象,可以与 Playwright 配合使用,就像与 WebdriverIO 或 Selenium WebDriver 配合使用一样,尽管直接使用这些 Web 浏览器集成工具时,它们会提供不同的接口和编程模型。

Note that in listing 15.2, interactions to click and to navigate come from the Serenity/JS Web module. This module is agnostic of the low-level integration tool used. More specifically, it provides a consistent abstraction that works as well with Playwright, as it does with WebdriverIO or Selenium WebDriver, even though, when used directly, those web browser integration tools offer different interfaces and programming models.

清单 15.2 中需要注意的另一件事是自定义辅助类Form,这使得识别 Flying High Airlines 应用程序用户界面的表单字段变得更加容易。我们将在第 15.2 节讨论实现测试自动化的集成层时详细研究这两者系统。

Another thing to note in listing 15.2 is the custom helper class called Form, which makes it easier to identify form fields of the Flying High Airlines app user interface. We’ll investigate both in detail in section 15.2, when we discuss implementing the Integration layer of our test automation system.

15.1.4 使用由外而内的方法实现任务替代

15.1.4 Using an outside-in approach to enable task substitution

我们讨论实现集成层,让我们指出由外而内的方法的另一个好处:从外部参与者的角度定义验收测试任务,并命名任务来描述它们有助于实现的业务领域目标,而不是它们如何实现。

Before we discuss implementing the Integration layer, let’s point out one more benefit of our outside-in approach: defining acceptance test tasks from the perspective of an external actor and naming the tasks to describe the business domain goal they help to accomplish, as opposed to how they do it.

如果始终如一地使用这种由外而内的方法,则可以用一项任务替代另一项任务,而不会影响依赖它的测试。当然,前提是所讨论的任务实现的是相同的目标。例如,使用两个交互来定义查找注册表单的任务是正确的,一个交互导航到主页,另一个交互单击注册按钮,如清单 15.2 所示。但是,也可以使用直接导航到注册页面的单个交互来定义替代实现,这将实现相同的目标,但速度会更快一些,如下面的清单所示。

When used consistently, this outside-in approach enables substituting one task for another without affecting the tests that rely on it. That is, of course, if the tasks in question accomplish the same goal. For example, it’s correct to define the task to locate the registration form using two interactions, one to navigate to the home page and one to click on the registration button, as per listing 15.2. However, it would also be acceptable to define an alternative implementation using a single interaction that navigates directly to the registration page instead, which would accomplish the same goal but be a bit faster, as per the following listing.

清单 15.3 任务的替代实现LocateRegistrationForm

Listing 15.3 Alternative implementation of the task to LocateRegistrationForm

从 '@serenity-js/core' 导入 { Task }
从 '@serenity-js/web' 导入 { Navigate }
const LocateRegistrationForm = () =>
  Task.where(`#actor 定位注册表单`,
      Navigate.to('/register'),
import { Task } from '@serenity-js/core'
import { Navigate } from '@serenity-js/web'
const LocateRegistrationForm = () =>
  Task.where(`#actor locates the registration form`,
      Navigate.to('/register'),
  )

然而,有些场景可能不仅要求我们出于性能或类似原因将一种实现完全替换为另一种实现,而且还要求我们同时为给定任务提供几种替代实现。通常,当我们需要表示实现同一目标的不同方法时,就会发生这种情况。

Some scenarios, however, might require us to not only replace one implementation with another altogether for performance or similar reasons, but to have several alternative implementations of a given task available simultaneously. Typically, this happens when we need to represent different means of achieving the same goal.

在这些情况下,将给定任务的替代实现分组到一个以该任务有助于实现的目标命名的类下会很有用。在我们的示例中,这样的类可以称为LocateRegistrationForm并使其方法名称表明替代实现的不同之处(例如,LocateRegistrationForm.viaHomePageLocateRegistrationForm.viaDirectNavigation)。下面的清单显示了实现此模式的一个示例。

In those cases, it can be useful to group the alternative implementations of a given task under a single class named after the goal the task helps to accomplish. In our example, such a class could be called LocateRegistrationForm and have its method names indicate what makes the alternative implementations different (e.g., LocateRegistrationForm.viaHomePage or LocateRegistrationForm.viaDirectNavigation). An example of implementing this pattern is shown in the following listing.

清单 15.4 对同一任务的替代实现进行分组

Listing 15.4  Grouping alternative implementations of the same task

从 '@serenity-js/core' 导入 { Task };
从 '@serenity-js/web' 导入 { Click, Navigate };
 
导出类 LocateRegistrationForm {
    静态 viaHomePage = () =>
        Task.where(`#actor 通过主页定位注册表单`,
            导航至('/'),
            Click.on(Form.buttonCalled('注册')),
        (英文):
 
    静态 viaDirectNavigation = () =>
        Task.where(`#actor 通过直接导航注册表格`,
            Navigate.to('/register'),
        (英文):
}
import { Task } from '@serenity-js/core';
import { Click, Navigate } from '@serenity-js/web';
 
export class LocateRegistrationForm {
    static viaHomePage = () =>
        Task.where(`#actor locates registration form via home page`,
            Navigate.to('/'),
            Click.on(Form.buttonCalled('Register')),
        );
 
    static viaDirectNavigation = () =>
        Task.where(`#actor registration form via direct navigation`,
            Navigate.to('/register'),
        );
}

这种用一项任务替代另一项任务以实现相同目标的想法适用于所有抽象级别,也是混合测试的基础,我们将在后面讨论下一个。

This idea of substituting one task for another that accomplishes the same goal works at all the levels of abstraction and is the foundation of blended testing, which we’ll discuss next.

15.1.5 利用混合测试实现非 UI 交互

15.1.5 Leveraging non-UI interactions with blended testing

尽管Serenity/JS 提供了多种机制来简化与 Web 界面的交互,但请记住,一般来说,与 Web UI 交互往往比直接与这些 UI 使用的任何后端服务交互要慢得多,也更棘手。在本节中,我们将介绍混合测试:在单一场景中与被测系统的不同外部接口进行交互,以发挥它们的优势。

While Serenity/JS provides numerous mechanisms to make interacting with web interfaces easier, it’s always useful to remember that, in general, interacting with web UIs tends to be considerably slower and more tricky than interacting directly with any backend services those UIs use. In this section we’ll look at blended testing: interacting with different external interfaces of the system under test in a single scenario to play to their strengths.

基于 Web 的系统通常提供至少两种主要类型的外部接口:Web UI 和基于 HTTP 的编程 API,例如 REST 或 GraphQL。由于 Web UI 针对与人交互的体验进行了优化,因此它们通常会应用用户体验模式,这会使它们难以与自动化测试进行交互。例如,一份冗长的保险索赔表可以分成多个页面,以帮助用户了解他们在流程中的位置。在线支付系统可能会引入人为延迟,以帮助用户感觉到交易更安全,因为它“需要更长的时间”。UI 还经常使用动画来使交互更具吸引力,等等。这些模式中的每一种都会增加自动化测试与 UI 交互所需的时间。

A web-based system typically offers at least two main types of external interfaces: web UI and HTTP-based programmatic APIs, such as REST or GraphQL. Since web UIs are optimized for the experience of a human interacting with them, they often apply user experience patterns that can make them difficult to interact with for an automated test. For example, a lengthy insurance claim form could be split across numerous pages to help the user make sense of where they are in the process. An online payment system might introduce artificial delays to help make its users feel that a transaction is more secure since it “takes longer.” It is also common for UIs to use animations to make interacting with them more engaging, and so on. Each one of those patterns increases the amount of time an automated test needs to spend interacting with the UI.

对于 Flying High Airlines 应用程序,每个旅行者都需要注册一个帐户才能使用该系统。当然,让自动测试至少填写一次基于 Web 的注册表单以确保注册过程按预期为用户工作是完全合理的。但是,在我们的测试套件中对每个场景都这样做以执行测试数据设置会很浪费。

In the case of the Flying High Airlines app, every traveler needs to register an account in order to use the system. It is perfectly sensible, of course, to have an automated test fill out the web-based registration form at least once to ensure that the registration process works as expected for users. However, doing so for every single scenario in our test suite to perform test data setup would be wasteful.

更好的方法是利用您可能还记得的图 14.11 中的注册 API。使用后端 API 注册测试用户帐户将大大加快需要注册帐户但并不特别介意如何创建该帐户的 UI 测试场景。

A better way to approach it is to leverage the registration API you might remember from figure 14.11. Using a backend API to register test user accounts will considerably speed up UI test scenarios that require a registered account but that don’t particularly mind how that account gets created.

这里非常有效的一个简单惯例是使用When关键字主动语态对于需要演示如何完成给定步骤的步骤,并使用Given关键字被动语态对于仅关心某事已完成的步骤。

A simple convention that works quite well here is to use the When keyword and active voice for steps that need to demonstrate how a given step is done and use the Given keyword and passive voice for steps that only care about that something is done.

比如我们以用户注册账号这个步骤为例,按照这个惯例,下面这个步骤描述的是一个前提条件,并不一定关心 Tracy 是怎么注册的,只要她注册了就可以了:

For example, let’s take the step where the user registers an account. According to this convention, the following step describes a precondition and doesn’t necessarily care about how Tracy has signed up, as long as she has signed up:

鉴于Tracy 已经签约
Given Tracy has signed up

另一方面,描述动作并使用主动语态表达的步骤演示了我们想要演示的场景的一部分:

On the other hand, a step describing an action and expressed using active voice demonstrates the part of the scenario we want to demonstrate:

Tracy 报名时
When Tracy signs up

由于这些步骤使用两种语法语态来表达,因此在实现其相关步骤定义时很容易区分它们,因为它们使用不同的 Cucumber 表达式来匹配步骤。前提条件步骤可以委托给使用编程 API 注册参与者的任务,而操作步骤委托给通过 Web 界面注册参与者的任务:

Since the steps are expressed using two grammatical voices, it’s easy to differentiate between them when implementing their associated step definitions since they use a different Cucumber expression to match the step. The precondition step can delegate to a task registering the actor using a programmatic API, while the action step delegates to a task registering the actor via the web interface:

鉴于(‘{actor} 已注册’,(actor:Actor)=>
    演员.尝试(
        SignUp.viaApiUsing(
            注释 <TravelerNotes>().get('travelerDetails'),
        ),
    ))
 
(‘{actor} 注册’时,(actor:Actor)=>
    演员.尝试(
        注册.使用(
            注释 <TravelerNotes>().get('travelerDetails'),
        ),
    ))
Given('{actor} has signed up', (actor: Actor) =>
    actor.attemptsTo(
        SignUp.viaApiUsing(
            notes<TravelerNotes>().get('travelerDetails'),
        ),
    ))
 
When('{actor} signs up', (actor: Actor) =>
    actor.attemptsTo(
        SignUp.using(
            notes<TravelerNotes>().get('travelerDetails'),
        ),
    ))

任务的两个变体的实现SignUp如清单 15.5 所示,其中任务SignUp.viaApiUsing(travelerDetails)使用 @serenity-js/rest 模块提供的 HTTP 交互。请注意,该任务的两个变体SignUp接受的任务参数QuestionAdapter<T>,我们将在15.1.6节中讨论。

The implementation of the two variations of the task SignUp are shown in listing 15.5, where the task to SignUp.viaApiUsing(travelerDetails) uses HTTP interactions provided by the @serenity-js/rest module. Note that the two variations of the task SignUp accept a task parameter of QuestionAdapter<T>, which we’ll talk about in section 15.1.6.

清单 15.5 实现与不同接口交互的任务变体

Listing 15.5 Implementing variations of a task interacting with different interfaces

从 '@serenity-js/core' 导入 { Answerable, QuestionAdapter, Task }
从 '@serenity-js/rest' 导入 {Send, PostRequest}
从 '@serenity-js/assertions' 导入 { Ensure, equals }
 
注册类 {
  使用 =(travelerDetails: QuestionAdapter<TravelerDetails>) =>
    Task.where(`#actor 注册`,
      找到注册表格(),
      FillOutRegistrationForm.使用(旅行者详细信息),
      提交注册表单(),
 
  viaApiUsing = (travelerDetails: QuestionAdapter<TravelerDetails>) =>
    Task.where(`#actor 注册(通过 API)`,
      发送.a(PostRequest.to('/api/auth/register')
          .with(旅行者详情)),
      确保(LastResponse.status(),equals(201)),
}
import { Answerable, QuestionAdapter, Task } from '@serenity-js/core'
import { Send, PostRequest } from '@serenity-js/rest'
import { Ensure, equals } from '@serenity-js/assertions'
 
class SignUp {
  using =(travelerDetails: QuestionAdapter<TravelerDetails>) =>
    Task.where(`#actor signs up`,
      LocateRegistrationForm(),
      FillOutRegistrationForm.using(travelerDetails),
      SubmitRegistrationForm(),
    )
 
  viaApiUsing = (travelerDetails: QuestionAdapter<TravelerDetails>) =>
    Task.where(`#actor signs up (via API)`,
      Send.a(PostRequest.to('/api/auth/register')
          .with(travelerDetails)),
      Ensure.that(LastResponse.status(), equals(201)),
    )
}

值得注意的是,并非所有系统都提供可用于测试自动化目的的 HTTP 或类似 API。在这种情况下,值得考虑创建特定于测试的编程 API,以帮助完成特定于测试的任务,例如注册测试帐户、设置测试数据或访问有关被测系统状态的信息(这些信息可能无法通过用户获得)界面。

It’s worth noting that not all the systems provide HTTP or similar APIs that could be readily available for test automation purposes. In those cases, it’s worth considering creating test-specific programmatic APIs that help with test-specific tasks, such as registering test accounts, setting up test data, or accessing information about the state of the system under test that might not be available through the user interface.

系统级测试与组件测试

System-level tests versus component tests

在决定测试自动化方法时考虑混合测试等模式时,同样重要的是要考虑系统需要组装到何种程度才能进行测试以提供有意义的结果并让我们有足够的信心相信给定的功能能够按预期工作。

When considering patterns such as blended testing when deciding on your test automation approach, it is also important to consider the extent to which the system needs to be assembled for the test to provide meaningful results and give us sufficient confidence that a given feature works as expected.

一个常见的错误是仅针对部署到类似生产环境的完整组装系统执行所有验收测试。

A common mistake is to execute all the acceptance tests only against a fully assembled system deployed to a production-like environment.

一个好的替代方案是限制那些不需要整个系统存在的场景的组装级别,因此可以执行,例如,针对连接到模拟 Web 服务的 Web UI,或者针对使用 Storybook 或 Playwright 组件测试等工具呈现的单个 Web UI 组件。

A good alternative is to limit the level of assembly for those scenarios that don’t require the entire system to be present, and can therefore be executed, for example, against a web UI connected to mock web services, or against individual web UI components rendered using a tool like Storybook, or Playwright Component Tests.

15.1.6 使用任务作为代码重用的机制

15.1.6 Using tasks as a mechanism for code reuse

因为定位注册表单所需的活动被封装在一个任务中,任何依赖于它的其他任务(如注册任务)都无需担心其实现细节,只需担心它可以帮助参与者完成定位注册表单的一个小目标。当然,如果区分两种定位表单的方式很重要,我们可以轻松地将它们捕获为两个单独的任务,如清单 15.4 所示。

Because the activities required to locate the registration form are encapsulated within a single task, any other tasks that rely on it, like the task to sign up, don’t need to worry about its implementation details, only that it helps the actor to accomplish a mini goal of locating the registration form. Of course, if it were important to differentiate between the two ways of locating the form, we could easily capture them as two separate tasks, as per listing 15.4.

这种专注于使用不同抽象级别的任务来建模验收测试的设计方法有助于我们使代码更易于理解。当这些任务的名称使用我们的受众使用的相同领域特定词汇时,即使对于不熟悉编程或测试自动化的受众来说,我们的代码也变得易于理解。此外,任务使我们的代码更易于重用。这是因为“注册”任务可以插入到任何需要注册用户的工作流中,并且任何重用此类任务的测试都无需担心该用户如何找到注册表单,甚至无需担心他们是否使用了 Web 浏览器。我们的测试自动化系统的其他部分也是如此,它们只需要重用工作流的一部分,也许是为了模拟我们现在要讨论的错误情况。

This design approach of focusing on modeling our acceptance tests using tasks at different levels of abstraction helps us make our code much easier to understand. When the names of those tasks use the same domain-specific vocabulary our audience uses, our code becomes comprehensible even for audiences not used to programming or test automation in general. Additionally, tasks make our code much easier to reuse. That’s because a task to “sign up” could be plugged into any workflows that require a signed-up user, and any test reusing such a task doesn’t need to worry about how that user has found the registration form, or even if they used the web browser at all. The same goes for other parts of our test automation system that need to reuse only parts of the workflow, perhaps to simulate error conditions, which we’re going to discuss now.

要了解如何重用我们已经编写的任务,请考虑重复的电子邮件场景,该场景最初在清单 14.6 中描述,并在下面的清单中重复以方便您的使用。

To see how we’d go about reusing the tasks we’ve already written, consider the duplicate email scenario, originally described in listing 14.6, and repeated in the following listing for your convenience.

清单 15.6 负面场景:使用重复的电子邮件地址注册

Listing 15.6 Negative scenario: Signing up using duplicate email address

功能:注册
 
  规则:不允许使用重复的用户名
 
    场景:使用重复的电子邮件地址注册
 
      迈克·史密斯 (Mike Smith) 是现有的飞行常客会员。
      他的妻子珍妮·史密斯 (Jenny Smith) 没有飞行常客账户。
 
      假设Mike 已使用以下详细信息进行注册:
        | 电子邮件 | smiths@example.org |
       Jenny 尝试使用以下方式注册时:
        | 电子邮件 | smiths@example.org |
       然后她应该被告知一个错误:“电子邮件已存在”
Feature: Sign up
 
  Rule: Duplicate usernames are not allowed
 
    Scenario: Sign up using duplicate email address
 
      Mike Smith is an existing Frequent Flyer member.
      His wife Jenny Smith does not have a Frequent Flyer account.
 
      Given Mike has signed up using the following details:
        | email | smiths@example.org |
       When Jenny tries to sign up using:
        | email | smiths@example.org |
       Then she should be advised of an error: "Email exists"

在第一步中,我们希望被叫来的演员Mike有一个注册的飞行常客账户,以便下一步我们可以验证在Jenny尝试使用相同的电子邮件地址注册时是否遵守不允许重复的电子邮件地址的业务规则。

In the first step, we want the actor called Mike to have a registered Frequent Flyer account so that in the next step we can verify that the business rule of not allowing duplicate email addresses is adhered to when Jenny tries to sign up using the same email address.

注意虽然从技术上讲,我们可以让两个演员在他们的旅行者笔记中使用相同的电子邮件地址,但在功能文件中根本没有提及,但在场景本身中突出显示该地址可以帮助更好地表达观点并吸引读者对这个关键细节的注意。

NOTE While technically speaking we could have two actors with the same email address defined in their traveler notes and not mentioned in the feature file at all, highlighting the address in the scenario itself can help to get the point across better and draw readers’ attention to this key detail.

我们在清单 15.1 中定义的注册任务已经接受了一个任务参数TravelerDetails,因此看起来非常适合在重复电子邮件场景中重用:

The task to sign up we defined in listing 15.1 already accepts a task parameter of TravelerDetails, and thus looks like a good candidate to reuse in the duplicate email scenario:

const SignUp = (旅行者详细信息:旅行者详细信息) =>
    Task.where('#actor 注册',
        找到注册表格(),
        填写登记表(旅客详细信息),
        提交注册表单(),
const SignUp = (travelerDetails: TravelerDetails) =>
    Task.where('#actor signs up',
        LocateRegistrationForm(),
        FillOutRegistrationForm(travelerDetails),
        SubmitRegistrationForm(),
    )

我们已经拥有的 Cucumber 步骤定义还允许我们将场景中定义的包含旅行者详细信息的数据表传递给任务SignUp

The Cucumber step definition we already have in place also allows us to pass a data table with traveler details defined in the scenario to the task to SignUp:

(‘{actor} 使用以下详细信息注册:’,
    (演员:演员,数据:数据表)=>
        演员.尝试(
            注册(data.rowsHash()),
When('{actor} signs up using following details:',
    (actor: Actor, data: DataTable) =>
        actor.attemptsTo(
            SignUp(data.rowsHash()),
        )
)

但是,我们当前的步骤定义要求一次性提供所有旅行者的详细信息。这并不理想,因为它会严重扰乱特征文件并分散读者的注意力,使他们无法注意到重要的细节,更不用说这会在需要相同配置的场景中导致数据重复:

However, our current step definition requires all the traveler details to be provided in one go. This is not ideal as it would seriously clutter the feature file and distract the reader from noticing the important details, not to mention the duplication of data this would cause across scenarios that require the same configuration:

    场景:使用有效的旅行者详细信息进行注册
 
      Tracy 使用以下旅行者详细信息进行注册时:
        | 名字 | 特蕾西 |
        | 姓氏 | 旅行者 |
        | 电子邮件 | Tracy.Traveler@example.org |
        | 密码 | P@ssw0rd |
        | 标题 | Mx |
        | 地址 | 亚历山大维多利亚街 35 号 |
        | 国家 | 澳大利亚 |
        | 座位偏好 | 窗口 |
      然后她应该可以登录
    Scenario: Sign up using valid traveler details
 
      When Tracy signs up using following traveler details:
        | firstName      | Tracy                          |
        | lastName       | Traveler                       |
        | email          | Tracy.Traveler@example.org     |
        | password       | P@ssw0rd                       |
        | title          | Mx                             |
        | address        | 35 Victoria Street, Alexandria |
        | country        | Australia                      |
        | seatPreference | window                         |
      Then she should be able to sign in

从代码重用和可读性的角度来看,更好的方法是

A better approach from both code reuse and readability perspectives is to

  • 为每个演员提供一套数据集,描述他们所要代表的角色。

  • Provide each actor with a data set describing the persona they’re meant to represent.

  • 实现我们的 Cucumber 步骤定义,以便它们在调用任务时将此默认数据集作为任务参数注入。

  • Implement our Cucumber step definitions so that they inject this default data set as task argument when invoking the task.

  • 在需要模拟错误条件的场景中部分覆盖默认数据集。

  • Partially override the default data set in scenarios that need to simulate error conditions.

第一点可以通过为每个参与者提供以下能力来实现:TakeNotes,您第一次遇到是在 14.2.3 节中,这里重复了必要的转换配置:

The first point can be accomplished by providing each actor with an ability to TakeNotes, which you first encountered in section 14.2.3, with necessary cast configuration repeated here:

Actors实现Cast {
 
  构造函数    私人只读浏览器:playwright.Browser,
    私有只读baseURL:字符串  ){
  }
 
  准备(演员:演员):演员{
    返回actor.whoCan( 
      BrowseTheWebWithPlaywright.使用(.browser,{
        baseURL:这个.baseURL
      }),
      
      CallAnApi.at(.baseURL),
      
      TakeNotes.使用(
        Notepad.with<TravelerNotes>({
          旅行者详细信息:TravelerDetails.of(actor.name),
        }),
      ),
    (英文):
  }
}
class Actors implements Cast {
 
  constructor(
    private readonly browser: playwright.Browser,
    private readonly baseURL: string,
  ) {
  }
 
  prepare(actor: Actor): Actor {
    return actor.whoCan( 
      BrowseTheWebWithPlaywright.using(this.browser, {
        baseURL: this.baseURL
      }),
      
      CallAnApi.at(this.baseURL),
      
      TakeNotes.using(
        Notepad.with<TravelerNotes>({
          travelerDetails: TravelerDetails.of(actor.name),
        }),
      ),
    );
  }
}

班级TravelerDetails如清单 14.4 中最初所示,根据演员姓名动态生成有效的旅行者详细信息。这有助于我们避免在功能文件中明确指定它们:

The TravelerDetails class, as presented originally in listing 14.4, dynamically generates valid traveler details based on the actor’s name. This helps us to avoid having to specify them explicitly in our feature files:

导出抽象类 TravelerDetails {
  标题:字符串;     
  名字:字符串;      
  姓氏:字符串;  
  电子邮件:字符串;          
  密码:字符串;  
  地址:字符串;        
  国家:字符串;   
  座位偏好:字符串;
 
  静态(actorName:string):TravelerDetails {
    返回 {
      标题:'Mx',    
      名字:演员姓名,  
      姓氏:“旅行者”,
      电子邮件:`${ actorName }.Traveler@example.org`,
      密码:'P@ssw0rd',            
      地址:'35 Victoria Street, Alexandria',国家:'澳大利亚',
      座位偏好:‘靠窗’
    }    
  }
}
export abstract class TravelerDetails {
  title: string;     
  firstName: string;      
  lastName: string;  
  email: string;          
  password: string;  
  address: string;        
  country: string;   
  seatPreference: string;
 
  static of(actorName: string): TravelerDetails {
    return {
      title: 'Mx',    
      firstName: actorName,  
      lastName: 'Traveler',
      email: `${ actorName }.Traveler@example.org`,
      password: 'P@ssw0rd',            
      address: '35 Victoria Street, Alexandria', country: 'Australia',
      seatPreference: 'window'
    }    
  }
}

但是,要使用来自参与者记事本的旅行者详细信息作为 Cucumber 步骤定义中的任务参数,我们需要对当前实现进行一项更改。目前,我们的任务接受静态同步数据结构TravelerDetails

However, to use the traveler details coming from an actor’s notepad as task argument in our Cucumber step definition, we need to make one change to our current implementation. Right now, our task accepts a static and synchronous data structure of TravelerDetails:

const SignUp = (travelerDetails:TravelerDetails) => 任务
const SignUp = (travelerDetails: TravelerDetails) => Task

虽然这在所有数据已知并提前提供的情况下已经足够,但此签名与从 返回的数据不兼容Notepad。这是因为记事本使用动态异步数据结构允许您使用一致的 API 同时处理静态和动态/延迟加载的数据结构。为了实现这一点,记事本创建了一个名为QuestionAdapter,它包装了返回的数据结构。这要求我们更改任务的签名,如下所示:

While this is sufficient in cases where all the data is known and provided upfront, this signature is not compatible with the data returned from the Notepad. That’s because the notepad uses dynamic and asynchronous data structures to allow you to work with both static and dynamic/lazy-loaded data structures simultaneously using a consistent API. To make this work, the notepad creates a proxy object called a QuestionAdapter, which wraps the returned data structure. This requires us to change the signature of our task as follows:

从 '@serenity-js/core' 导入 { QuestionAdapter, Task }
 
const SignUp = (travelerDetails: QuestionAdapter<TravelerDetails>) => 任务
import { QuestionAdapter, Task } from '@serenity-js/core'
 
const SignUp = (travelerDetails: QuestionAdapter<TravelerDetails>) => Task

方便的是,所有内置的 Serenity/JS Screenplay API 都接受静态和动态参数。更新任务签名以接受后,QuestionAdapter<TravelerDetails>我们现在可以定义一个 Cucumber 步骤,使用旅行者在记事本中找到的信息来注册旅行者:

Conveniently, all built-in Serenity/JS Screenplay APIs accept static and dynamic arguments. With the task signature updated to accept a QuestionAdapter<TravelerDetails> we can now define a Cucumber step that registers the traveler using information found in their notepad:

(‘{actor} 使用有效的旅行者详细信息注册’时,(actor:Actor)=>
    演员.尝试(
        注册.使用(
            注释 <TravelerNotes>().get('travelerDetails')
        ),
    ));
When('{actor} signs up using valid traveler details', (actor: Actor) =>
    actor.attemptsTo(
        SignUp.using(
            notes<TravelerNotes>().get('travelerDetails')
        ),
    ));

我们还可以定义一个步骤来部分覆盖默认详细信息,以支持模拟错误情况。在这里,我们使用Question.fromObjectAPI,它允许我们合并静态,动态或部分动态的数据结构并生成QuestionAdapter

We can also define a step that partially overrides the default details to support simulating error conditions. Here, we use Question.fromObject API, which allows us to merge static, dynamic, or partially dynamic data structures and produce a QuestionAdapter:

从 '@serenity-js/core' 导入 { Actor, Question }
从 '@cucumber/cucumber' 导入 { When, DataTable }
 
(‘{actor} 尝试使用以下方式注册:’,(actor:Actor,数据:DataTable)=>
    演员.尝试(
        注册.使用(
            问题.fromObject<TravelerDetails>(
                注释 <TravelerNotes>().get('travelerDetails'),
                data.rowsHash() 作为 Partial<TravelerDetails>,
    ));
import { Actor, Question } from '@serenity-js/core'
import { When, DataTable } from '@cucumber/cucumber'
 
When('{actor} tries to sign up using:', (actor: Actor, data: DataTable) =>
    actor.attemptsTo(
        SignUp.using(
            Question.fromObject<TravelerDetails>(
                notes<TravelerNotes>().get('travelerDetails'),
                data.rowsHash() as Partial<TravelerDetails>,
            )
        )
    ));

如您所见,参数化任务提供了一种极好的代码重用机制,而任务参数提供了一种在测试工作流程中引入变化的简便方法。

As you can see, parameterized tasks offer an excellent mechanism for code reuse, and task arguments provide an easy way to introduce variations in the workflow exercised by the tests.

此外,QuestionAdapterAPI允许我们引用代理对象的字段和方法,并像使用常规静态数据结构一样使用它。我们引用的任何字段也将以递归方式包装在此类代理对象中,从而提供一致的编程模型。

Furthermore, the QuestionAdapter API allows us to reference fields and methods of the proxied object and work with it just like we would with a regular, static data structure. Any fields we reference will also get wrapped in such proxy objects recursively, providing a consistent programming model.

这意味着我们可以定义填写登记表的任务,如下所示,引用相关的旅行者详细信息以将其传递给子任务:

What this means is that we can define the task to fill out the registration form simply, as follows, referencing the relevant bits of traveler details to pass them to subtasks:

导出 const FillOutRegistrationForm =
  (旅行者详细信息:QuestionAdapter <TravelerDetails> | TravelerDetails)=>
    Task.where(`#actor 填写登记表`,
      指定电子邮件地址(travelerDetails.email),
      指定密码(travelerDetails.密码),
      指定称呼(travelerDetails.title),
      指定FirstName(travelerDetails.firstName),
      指定姓氏(travelerDetails.lastName),
      指定家庭地址(travelerDetails.address),
指定居住国(旅行者详细信息.国家),
指定座位偏好(travelerDetails.seatPreference),
切换新闻通讯订阅.off(),
切换条款与条件.on(),
export const FillOutRegistrationForm =
  (travelerDetails: QuestionAdapter<TravelerDetails> | TravelerDetails) =>
    Task.where(`#actor fills out the registration form`,
      SpecifyEmailAddress(travelerDetails.email),
      SpecifyPassword(travelerDetails.password),
      SpecifySalutation(travelerDetails.title),
      SpecifyFirstName(travelerDetails.firstName),
      SpecifyLastName(travelerDetails.lastName),
      SpecifyHomeAddress(travelerDetails.address),
SpecifyCountryOfResidence(travelerDetails.country),
SpecifySeatPreference(travelerDetails.seatPreference),
ToggleNewsletterSubscription.off(),
ToggleTermsAndConditions.on(),
) 

实现类似的 Cumber 步骤

Implementing similar Cucumber steps

在寻找代码重用的机会时,重要的是寻找我们已经编写的代码和步骤定义与即将编写的代码和步骤定义之间的相似之处和不同之处。例如,使用非默认电子邮件地址注册的步骤是使用数据表定义的:

When looking for opportunities for code reuse it’s important to look for both similarities and differences between the code and step definitions we’ve already written and those we’re about to write. For example, the step to sign up using a nondefault email address is defined using a data table:

  假设Mike 已使用以下详细信息进行注册: 
       | 电子邮件 | smiths@example.org |
  Given Mike has signed up using the following details: 
       | email | smiths@example.org |

看起来和我们已经进行的步骤非常相似:

And looks very similar to the step we already have:

  Tracy 使用有效的旅行者详细信息进行注册时
  When Tracy signs up using valid traveler details

虽然新步骤要求参与者使用非默认电子邮件地址,但所有其他详细信息(例如名字、姓氏或座位偏好)都应保留为默认值。

While the new step requires the actor to use a nondefault email address, all the other details, such as first name, last name, or seat preference, should remain at their default values.

您可能还注意到,我们的新步骤使用 Cucumber 数据表(http://mng.bz/vXV4),而不是更传统的方式内联指定步骤参数,例如

You might have also noticed that our new step uses a Cucumber data table (http://mng.bz/vXV4) rather than the more traditional way to specify step parameters inline, such as

  假设Mike 已使用电子邮件地址“smiths@example.org”进行注册
  Given Mike has signed up using email address of "smiths@example.org"

使用数据表的原因是,如果需要,它允许我们轻松地更改其他默认参数,并同时指定多个覆盖,而不必为每个覆盖定义单独的 Cucumber 步骤。

The reason for using a data table is that it allows us to easily alter other default parameters as well, if needed, and to specify multiple overrides at the same time without having to define a separate Cucumber step for each override.

如果我们想要覆盖中定义的其他默认属性TravelerDetails,我们可以扩展数据表以包含它们的名称和值:

If we wanted to override other default properties as defined in TravelerDetails, we could expand our data table to include their names and values as well:

  假设Mike 已使用以下详细信息进行注册:
    | 名字 | 迈克尔 |
    | 座位偏好 | 过道 |
    | 电子邮件 | smiths@example.org |
  Given Mike has signed up using the following details:
    | firstName      | Michael            |
    | seatPreference | aisle              |
    | email          | smiths@example.org |

使用数据表而不是内联步骤参数使得步骤更加灵活,因为我们可以根据场景覆盖不同的细节,而不必创建比必要更多的 Cucumber 步骤定义。

Using a data table rather than inline step parameters makes the step more flexible, as we can override different details depending on the scenario, without having to create more Cucumber step definitions than necessary.

15.1.7 实施验证任务

15.1.7 Implementing verification tasks

方面我们尚未讨论的自动化测试的另一个问题是,如何让它们验证系统的实际状态是否符合我们在场景中给定阶段的预期。与 Serenity BDD 类似,Serenity/JS 还提供了一个与 Screenplay Pattern 兼容的断言库,可以帮助实现这一点,该库作为 @serenity-js/assertions 模块的一部分提供https://www.npmjs.com/package/@serenity-js/assertions)。为了了解它在实践中是如何运作的,让我们再次考虑重复的电子邮件地址场景。

One aspect of automated tests that we haven’t discussed yet is how to make them verify if the actual state of the system is what we expect it to be at a given stage in the scenario. Similar to Serenity BDD, Serenity/JS also provides a Screenplay Pattern-compatible assertions library that can help with this and that is available as part of the @serenity-js/assertions module (https://www.npmjs.com/package/@serenity-js/assertions). To see how it works in practice, let’s consider the duplicate email address scenario again.

在我们的场景中,当参与者提供的注册详细信息中包含系统中已存在的电子邮件地址时,用户界面会显示一条错误消息,如图 15.2 所示。错误消息是toast:当参与者提交表单时显示的简短临时通知,之后不久就会消失。这种用户体验设计意味着,为了验证向参与者显示的建议(错误消息的文本),我们必须做三件事:

In our scenario, when the actor provides registration details containing an email address that’s already present in the system, the user interface shows an error message, as depicted in figure 15.2. The error message is a toast: a brief, temporary notification that’s shown when the actor submits the form, and that disappears shortly afterward. This user experience design means that to verify the advice shown to the actor, the text of the error message, we have to do three things:

  1. 通过提交包含系统中已存在的电子邮件地址的注册表单来触发错误状态。我们可以使用我们的任务来执行此操作,并使用包含重复电子邮件地址的参数SignUp进行参数化。TravelerDetails

  2. Trigger the error state by submitting a registration form with an email address that’s already present in the system. We can do this using our task to SignUp, parameterized with a TravelerDetails argument containing a duplicate email address.

  3. 接下来,我们需要等待错误消息出现在屏幕上,这会在一个短暂的动画之后发生。

  4. Next, we need to wait for the error message to appear on the screen, which happens after a short animation.

  5. 然后,验证消息的文本是否符合我们期望看到的内容。

  6. Then, verify that the text of the message meets what we expected to see.

  7. 最后,通过关闭消息来避免等待过渡动画。

  8. And finally, avoid having to wait for the transition animation by dismissing the message.

图 15.2 重复电子邮件地址场景中显示的错误信息是提交表单时显示的简短临时通知。

Figure 15.2 Error message displayed in the duplicate email address scenario is a brief, temporary notification displayed when the form is submitted.

虽然我们将在第 15.2 节详细讨论与 Web 界面的交互,但让我们先介绍一些基础知识,并了解如何使我们的测试场景与烤面包机小部件生成的 HTML 结构交互以验证其状态。小部件的 HTML 结构如下所示:

While we’ll discuss interacting with web interfaces in detail in section 15.2, let’s cover some of the basics already and look at how we can make our test scenario interact with the HTML structure generated by the toaster widget to verify its state. The HTML structure of the widget looks as follows:

<div class="ngx-toastr toast-error">
    <div class="toast-message">
        电子邮件已存在,请尝试其他名称
    </div>
</div>
<div class="ngx-toastr toast-error">
    <div class="toast-message">
        Email exists, please try another name
    </div>
</div>

Serenity/JS 使用页面元素的概念来表示与测试场景交互的 Web 元素。页面元素使用元素选择器来标识,例如By.cssBy.xpath等等,并可选择给出在报告与元素的交互时使用的自定义描述。我们可以定义一个表示烤面包机消息的页面元素,如下所示:

Serenity/JS uses the concept of page elements to represent web elements the test scenario interacts with. A page element is identified using an element selector, such as By.css, By.xpath, and so on, and optionally given a custom description to be used when reporting interactions with the element. We could define a page element representing the toaster message like this:

从 '@serenity-js/web' 导入 { By, PageElement }
 
const ToasterMessage = () =>
    PageElement.located(By.css(`.ngx-toastr > .toast-message`))
        .writtenAs('烤面包机消息')
import { By, PageElement } from '@serenity-js/web'
 
const ToasterMessage = () =>
    PageElement.located(By.css(`.ngx-toastr > .toast-message`))
        .describedAs('toaster message')

注意虽然 Serenity/JSBy选择器旨在提供类似于您可能熟悉的其他框架(如 Selenium WebDriver)的“外观和感觉”,它们与集成工具无关。这意味着无论您决定使用哪种底层集成工具,您都可以使用相同的页面元素定义。我们将在第 15.2.2 节详细研究这种设计。

NOTE While Serenity/JS By selectors are designed to provide a similar “look and feel” to what you might be familiar with from other frameworks like Selenium WebDriver, they are integration tool agnostic. This means you can use the same page element definitions no matter the underlying integration tool you decide to use. We’ll investigate this design in detail in section 15.2.2.

现在我们已经定义了页面元素来识别烤面包机消息,我们可以使用 Serenity/JSAssertions实现清单 15.6 中的最后一个场景步骤:

Now that we’ve defined the page element to identify the toaster message, we can use the Serenity/JS Assertions library to implement the last scenario step from listing 15.6:

然后她应该被告知一个错误:“电子邮件已存在”
Then she should be advised of an error: "Email exists"

具体来说,我们需要以下 API:

Specifically, we’ll need the following APIs:

  • Wait.until通知参与者等待,直到满足给定的期望

  • Wait.until to inform the actor to wait until the given expectation is met

  • Ensure.that验证烤面包机消息的文本是否符合我们的预期

  • Ensure.that to verify that the text of the toaster message is what we expected

  • Click.on忽略该消息

  • Click.on to dismiss the message

如果我们在 Cucumber 步骤定义中直接使用这些 API,其实现可能如下所示:

If we were to use those APIs directly in our Cucumber step definition, its implementation could look as follows:

从 '@serenity-js/core' 导入 { Actor, Wait }
从 '@serenity-js/web' 导入 { 点击 }
从 '@serenity-js/assertions' 导入 { Ensure, includes, isPresent, not }
 
然后(‘{pronoun} 应该被告知一个错误:{string}’,
  (演员:演员,预期消息:字符串)=>
    演员.尝试(
      // 等待消息出现
      等待.直到(ToasterMessage(),isPresent()),
 
      // 验证消息
      确保(Text.of(ToasterMessage()),包括(expectedMessage)),
 
      // 关闭消息
      点击(ToasterMessage()),
 
      // 等待消息消失
      等待.直到(ToasterMessage(),而不是(isPresent())),
import { Actor, Wait } from '@serenity-js/core'
import { Click } from '@serenity-js/web'
import { Ensure, includes, isPresent, not } from '@serenity-js/assertions'
 
Then('{pronoun} should be advised of an error: {string}',
  (actor: Actor, expectedMessage: string) =>
    actor.attemptsTo(
      // wait for the message to appear
      Wait.until(ToasterMessage(), isPresent()),
 
      // verify the message
      Ensure.that(Text.of(ToasterMessage()), includes(expectedMessage)),
 
      // dismiss the message
      Click.on(ToasterMessage()),
 
      // wait for the message to disappear
      Wait.until(ToasterMessage(), not(isPresent())),
  )
)

然而,虽然这种实现方式有效,但它远非理想,因为它违反了我们在第 14.2.1 节中讨论的设计原则,即规范层(Cucumber 步骤定义)的组件不直接调用接口特定的集成层(低级 Web 交互)的组件。为了避免违反这一设计原则,我们可以遵循第 15.1.3 节中讨论的相同模式,即将低级交互组合成更高级别的、特定于业务领域的验证任务。这种改进的实现可能如下所示:

However, while this implementation works, it’s far from ideal as it violates the design principle we discussed in section 14.2.1, the one for the component of the specification layer (the Cucumber step definition) not to directly invoke components of the interface-specific Integration layer (the low-level web interactions). To avoid violating this design principle, we can follow the same pattern we discussed in section 15.1.3, which was to compose the low-level interactions into higher-level, business domain-specific verification tasks. Such improved implementation could look like this:

然后(‘{pronoun} 应该被告知一个错误:{string}’, 
  (演员:演员,预期消息:字符串)=>
    演员.尝试(
      VerifySubmission.failedWith(预期消息),
Then('{pronoun} should be advised of an error: {string}', 
  (actor: Actor, expectedMessage: string) =>
    actor.attemptsTo(
      VerifySubmission.failedWith(expectedMessage),
  )
)

这种更高级别、特定于业务领域的验证任务提供了一种在代码中反映特定于业务的概念的好方法;将“验证提交失败并显示预期消息”的清晰度与“等待、检查、单击、等待”的低级交互序列进行比较。

Such higher-level, business domain-specific verification tasks provide a great way to reflect business-specific concepts in our code; compare the clarity of “verify submission failed with an expected message” versus the low-level sequence of interactions to “wait, check, click, wait.”

编写断言的另一个优点是(Ensure.that) 以及任何关联的同步语句 (Wait.until)分解为更高级别的任务的优点在于,它提供了一种方便的机制,可以在 Cucumber 步骤和其他任务之间重用代码。此外,我们可以应用您在清单 15.4 中第一次遇到的模式,将给定验证任务的变体收集到一个类下。这样,我们就可以得到代表验证提交任务的更高级别的类,其中有两种可能的变体来验证成功或失败:

Another advantage of composing assertions (Ensure.that) and any associated synchronization statements (Wait.until) into higher-level tasks is that it offers a convenient mechanism for code reuse across both Cucumber steps and other tasks. Furthermore, we can apply the pattern you first encountered in listing 15.4 to gather variations of a given verification task under one class. This way we could arrive at the higher-level class representing tasks to verify submission, with two possible variations of verifying either success or failure:

类验证提交 {
  静态成功(expectedMessage:string):任务{
    /* ... */
  }
 
  静态 failedWith(expectedMessage:string):任务{
    /* ... */
  }
}
class VerifySubmission {
  static succeededWith(expectedMessage: string): Task {
    /* ... */
  }
 
  static failedWith(expectedMessage: string): Task {
    /* ... */
  }
}

然后,我们可以将验证成功提交的任务合并到演员使用有效旅行者详细信息进行注册的步骤中:

We can then incorporate the task to verify successful submission into the step for the actor to sign up using valid traveler details:

(‘{actor} 使用有效的旅行者详细信息进行注册’时, 
  (演员:演员) =>
    演员.尝试(
      注册.使用(
        注释 <TravelerNotes>().get('travelerDetails')
      ),
      VerifySubmission.succeededWith('注册成功'),
When('{actor} signs up using valid traveler details', 
  (actor: Actor) =>
    actor.attemptsTo(
      SignUp.using(
        notes<TravelerNotes>().get('travelerDetails')
      ),
      VerifySubmission.succeededWith('registered successfully'),
    )
)

我们还可以在清单 15.6 的步骤中使用验证任务的失败案例变体,检查向用户显示的错误消息。毕竟,向用户显示的错误消息是与客户进行业务沟通的重要组成部分,因此将围绕它们的验收标准呈现到规范层很有用:

We can also use the failure case variation of our verification task in the step from listing 15.6 that checks the error message shown to the user. After all, error messages shown to the user form an important part of business communication with the customers, so it’s useful to surface the acceptance criteria around them to the Specification layer:

然后她应该被告知一个错误:“电子邮件已存在”
Then she should be advised of an error: "Email exists"

为了验证错误消息,我们可以按如下方式更新步骤定义:

To verify the error message, we can update the step definition as follows:

然后(‘{pronoun} 应该被告知一个错误:{string}’, 
  (演员:演员,预期消息:字符串)=>
    演员.尝试(
      VerifySubmission.failedWith(预期消息),
Then('{pronoun} should be advised of an error: {string}', 
  (actor: Actor, expectedMessage: string) =>
    actor.attemptsTo(
      VerifySubmission.failedWith(expectedMessage),
    )
)

验证注册表单提交成功或失败的任务的最终实现如下清单所示。这里,验证成功和失败变体的任务都由三个子任务组成,用于验证消息、其通知状态(表现为添加到小部件的 CSS 类并影响其颜色)以及关闭它。

The finished implementation of the tasks to verify success or failure of the registration form submission is shown in the following listing. Here, both the tasks to verify successful and failed variation are composed of three subtasks to verify the message, its notification status (manifesting itself as a CSS class added to the widget and affecting its color), and to dismiss it.

清单 15.7 实现验证任务

Listing 15.7 Implementing verification tasks

进口 {
  确保、等于、包括、存在、不
} 来自'@serenity-js/assertions';
从 '@serenity-js/core' 导入 { Task, Wait };
从 '@serenity-js/web' 导入 { Click, Text };
从'./Toaster 导入 { Toaster };
 
导出类 VerifySubmission {
  静态成功(expectedMessage:string){
    return Task.where(`#actor 确认表单提交成功`,
      VerifySubmission.hasMessage(预期消息),
      VerifySubmission.hasStatus('成功'),
      VerifySubmission.dismissMessage(),
    (英文):
  }
 
  静态 failedWith(expectedMessage:字符串){
    返回 Task.where(`#actor 确认表单提交失败`,
      VerifySubmission.hasMessage(预期消息),
      VerifySubmission.hasStatus('错误'),
      VerifySubmission.dismissMessage(),
    (英文):
  }
 
  私有静态 hasMessage(消息:字符串){
    返回 Task.where(`#actor 确认通知包含 ${ message }`,
      等待.直到(Toaster.message(),isPresent()),
      确保(Toaster.message()的文本包括(expectedMessage)),
    (英文):
  }
 
  私有静态 hasStatus(状态:'成功' | '错误'){
    返回 Task.where(`#actor 确认表单提交 ${ status }`,
      等待.直到(Toaster.message(),isPresent()),
      确保(Toaster.status(),equals(status)),
    (英文):
  }
 
  私有静态解除消息(){
    返回 Task.where(`#actor 关闭消息`,
      等待.直到(Toaster.message(),isPresent()),
      点击(Toaster.message()),
      等待.直到(Toaster.message(),而不是(isPresent())),
    (英文):
  }
}
import {
  Ensure, equals, includes, isPresent, not
} from '@serenity-js/assertions';
import { Task, Wait } from '@serenity-js/core';
import { Click, Text } from '@serenity-js/web';
import { Toaster } from './Toaster;
 
export class VerifySubmission {
  static succeededWith(expectedMessage: string) {
    return Task.where(`#actor confirms successful form submission`,
      VerifySubmission.hasMessage(expectedMessage),
      VerifySubmission.hasStatus('success'),
      VerifySubmission.dismissMessage(),
    );
  }
 
  static failedWith(expectedMessage: string) {
    return Task.where(`#actor confirms failed form submission`,
      VerifySubmission.hasMessage(expectedMessage),
      VerifySubmission.hasStatus('error'),
      VerifySubmission.dismissMessage(),
    );
  }
 
  private static hasMessage(message: string) {
    return Task.where(`#actor confirms notification includes ${ message }`,
      Wait.until(Toaster.message(), isPresent()),
      Ensure.that(Text.of(Toaster.message()), includes(expectedMessage)),
    );
  }
 
  private static hasStatus(status: 'success' | 'error') {
    return Task.where(`#actor confirms form submission ${ status }`,
      Wait.until(Toaster.message(), isPresent()),
      Ensure.that(Toaster.status(), equals(status)),
    );
  }
 
  private static dismissMessage() {
    return Task.where(`#actor dismisses the message`,
      Wait.until(Toaster.message(), isPresent()),
      Click.on(Toaster.message()),
      Wait.until(Toaster.message(), not(isPresent())),
    );
  }
}

正如你可能在清单 15.7 中注意到的,Serenity/JS Assertions 模块的期望如下isPresent()equals(), 或者includes(),兼容并可与两个同步语句一起使用(Wait.until) 和核实声明(Ensure.that)。Serenity/JS 在框架的各个部分都使用这种模式,以便在您需要根据期望是否得到满足做出决策时提供一致的编程体验。正如您很快将在第 15.2 节中看到的那样,期望也是 Serenity/JS 页面元素查询语言的基础,它为您提供了一种可移植的方式来识别感兴趣的页面元素。

As you might have noticed in listing 15.7, expectations from the Serenity/JS Assertions module, such as isPresent(), equals(), or includes(), are compatible and can be used together with both synchronization statements (Wait.until) and verification statements (Ensure.that). Serenity/JS uses this pattern throughout the various parts of the framework to offer a consistent programming experience whenever you need to make a decision, depending on whether an expectation is met. As you’ll soon see in section 15.2, expectations are also the foundation of the Serenity/JS Page Element Query Language, which gives you a portable way to identify page elements of interest.

关于清单 15.7 需要注意的第二件事是 Serenity/JS 同步语句,例如Wait.until(value, expectation)或者Wait.for(duration)是 Serenity/JS Core 模块的一部分,与 Web 测试无关。这意味着您可以使用它们,例如,不断轮询 REST API,直到它返回符合您期望的响应。在测试异步处理数据的批处理系统时,此模式非常有用。

The second thing to note about listing 15.7 is that Serenity/JS synchronization statements, such as Wait.until(value, expectation) or Wait.for(duration), are part of the Serenity/JS Core module and are not tied to web testing. This means you can use them, for example, to keep polling a REST API until it returns a response that meets your expectation. This pattern can be useful when testing batch processing systems that process data asynchronously.

第三件需要注意的事情是,清单 15.7 引用了一个名为 Toaster 的类,它代表烤面包机小部件,以及它的两个公共方法,Toaster.messageToaster .status分别表示小部件的有趣可交互元素或有关它的信息。此类称为精益页面对象,这种模式在实现可移植的基于 Web 的验收测试时对您很重要,因为它有助于弥合业务域层和集成层之间的差距。让我们讨论一下下一个。

The third thing to note is that listing 15.7 references a class called Toaster, representing the toaster widget, and its two public methods, Toaster.message and Toaster .status, represent the interesting interactable elements of the widget, or the information about it, respectively. This class is called a Lean Page Object, and this pattern will be important for you when implementing portable web-based acceptance tests as it helps to bridge the gap between the business Domain layer and the Integration layer. Let’s discuss it next.

15.2 设计可移植的集成层

15.2 Designing a portable Integration layer

测试自动化系统的集成层负责将其与被测系统的外部接口集成。这些通常涉及 Web 接口、移动应用程序或编程 API,例如 REST 或 GraphQL。但是,除了被测系统之外,测试自动化系统还需要与其测试执行环境、测试基础设施集成,并且可能违反直觉地与执行测试和分析其结果的人员和流程集成。在本节中,我们将讨论与这些集成相关的一些常见挑战,并提出解决方案来帮助您在项目中克服这些挑战。

The Integration layer of a test automation system is responsible for integrating it with the external interfaces of the system under test. Those typically involve web interfaces, mobile apps, or programmatic APIs, such as REST or GraphQL. However, apart from the system under test, a test automation system also needs to integrate with its test execution environment, test infrastructure, and perhaps counterintuitively, with people and processes executing the tests and analysing their results. In this section we’ll discuss some of the common challenges related to those integrations and propose solutions to help you overcome them on your projects.

15.2.1 为 Web 界面编写可移植的测试

15.2.1 Writing portable tests for the web interfaces

基于网络测试可以说是最具挑战性的集成测试类型之一。部分原因是测试范围往往很广,尤其是基于工作流的验收测试。它也往往很深入,因为并非每个系统都允许将其用户界面层与后端组件隔离进行测试,这可能导致此类后端组件造成干扰或管理开销。

Web-based testing is arguably one of the most challenging types of integration testing there is. This is partially because the scope being tested tends to be broad, particularly with the workflow-based acceptance testing. It also tends to be deep, as not every system allows for its user interface layer to be tested in isolation from the backend components, which can result in such backend components causing interference or management overhead.

除此之外,流行的 Web 测试集成工具(如 Selenium WebDriver、WebdriverIO、Puppeteer、Playwright 或 Cypress)虽然本身很出色,但各自使用不同的编程模型和不同的 API。这种跨工具不兼容性导致为一种测试集成工具编写的测试代码无法与另一种测试集成工具一起使用。

Apart from that, the popular web test integration tools, like Selenium WebDriver, WebdriverIO, Puppeteer, Playwright, or Cypress, as great as they are on their own, each use different programming models and different APIs. This cross-tool incompatibility prevents test code written for one test integration tool from being used with another.

当我们希望测试代码能够在典型系统的各种测试套件之间移植时,这种不兼容性也会成为问题。例如,我们可能希望使用 Playwright 来实现 UI 组件测试,与独立运行的 UI 小部件进行交互。我们可能还希望将 Playwright 用作烟雾测试(针对已部署系统执行的基本工作流测试)的集成工具,以确保关键功能按预期运行。由于 Playwright ( https://playwright.dev/ ) 执行速度快、开发人员体验良好,并且支持现代渲染引擎,因此这种工具选择往往效果很好。

This incompatibility also becomes problematic when we want our test code to be portable across the various test suites we have for a typical system. For example, we might want to use Playwright to implement UI component tests, interacting with UI widgets running in isolation. We might also want to use Playwright as an integration tool for our smoke tests—basic workflow tests executed against a deployed system—making sure that the critical features work as intended. This tool choice tends to work well since Playwright (https://playwright.dev/) offers fast execution, good developer experience, and supports modern rendering engines.

然而,典型的基于 Web 的系统(尤其是在大型组织中)不仅需要支持现代渲染引擎,还需要支持一些以前的版本。由于这不是 Playwright 支持的用例,这意味着对于那些高级、跨浏览器兼容性重点测试,我们需要使用不同的集成工具。

However, a typical web-based system, especially at larger organizations, needs to support not just the modern rendering engines, but also some of the previous versions. Since this is not a use case Playwright supports, it means that for those high-level, cross-browser compatibility-focused tests we need to use a different integration tool.

许多组织还投资了内部或第三方 Selenium 网格,以集中管理测试中使用的 Web 浏览器。在这些情况下,Selenium WebDriver 或 WebdriverIO 等集成工具更为合适,因为它们原生支持 WebDriver 协议 ( https://www.w3.org/TR/webdriver/ ),为在远程 Web 浏览器网格上运行跨浏览器测试提供了出色的支持。

Many organizations have also invested in in-house or third-party Selenium grids to centralize the management of web browsers used in the tests. In those cases, an integration tool like Selenium WebDriver or WebdriverIO is more appropriate, since thanks to their native support of the WebDriver protocol (https://www.w3.org/TR/webdriver/) they offer excellent support for running cross-browser tests on remote web browser grids.

您可能已经猜到了,与其编写大量测试套件(每个套件都与特定的测试集成工具绑定),不如在集成工具和其余测试自动化系统之间引入一个抽象层,这样可移植性更强。这样的抽象层可以让我们的代码在较低级别的集成工具之间可移植,然后可以根据给定的上下文选择最合适的集成工具。在我们的示例中,我们可以使用 Playwright 进行组件测试,使用 WebdriverIO 进行跨浏览器测试,而无需更改测试代码。(本章的代码示例存储库演示了同时使用 Playwright 和 WebdriverIO:http://mng.bz/49Yj。)

As you might have already guessed, instead of writing numerous test suites, each tied to a specific test integration tool, a far more portable design is to introduce an abstraction layer between the integration tools and the rest of our test automation system. Such an abstraction layer can enable us to make our code portable across the lower-level integration tools, which then allows for the most appropriate integration tool to be chosen for the given context. In our example, we could use Playwright for component tests and WebdriverIO for cross-browser tests without having to change the test code. (Using both Playwright and WebdriverIO simultaneously is demonstrated in the repository with code samples for this chapter: http://mng.bz/49Yj.)

在低级集成工具和测试自动化系统的其余部分之间引入抽象层也有助于我们避免集成工具锁定。这对于拥有数百或数千个测试的大型软件项目尤其重要,因为它可以降低特定集成工具的维护者停止支持该工具的风险。2又是因为考虑到可移植性的集成层允许我们切换到替代集成工具,而无需重写我们已经拥有的所有测试。

Introducing an abstraction layer between the lower-level integration tools and the rest of our test automation system also helps us to avoid integration tool lock-in. This is particularly important for the larger software projects with hundreds or thousands of tests, as it mitigates the risk of the scenario where the maintainers of a given integration tool stop to support it.2 That’s again because an Integration layer designed with portability in mind allows us to switch to an alternative integration tool without having to rewrite all the tests we already have.

但是,为了使 Web 集成层可移植,它需要抽象底层测试集成工具,并提供可在各种实现中一致且可靠地使用的编程模型和 API。现在让我们看看 Serenity/JS 提出的可移植性机制,以及如何使用它们使您的测试自动化系统与集成无关工具。

For the web Integration layer to be portable, though, it needs to abstract the underlying test integration tool and provide both a programming model and APIs that can be used consistently and reliably across the various implementations. Let’s now look at the portability mechanisms proposed by Serenity/JS and how you can use them to make your test automation system agnostic of the integration tool.

15.2.2 识别页面元素

15.2.2 Identifying page elements

Serenity/JS Web 模块 ( https://serenity-js.org ) 提供了用于与 Web 界面交互的 Screenplay Pattern API。其与定位 Web 元素相关的主要抽象包括

The Serenity/JS web module (https://serenity-js.org) provides Screenplay Pattern APIs for interacting with web interfaces. Its main abstractions relevant in the context of locating web elements are

  • Page,为 Web 浏览器选项卡建模

  • Page, modeling a Web browser tab

  • PageElement,对单个网络元素进行建模(http://mng.bz/Qnzv

  • PageElement, modeling a single web element (http://mng.bz/Qnzv)

  • PageElements,对 Web 元素集合进行建模并提供页面元素查询语言 API(参见第 15.2.6 节)

  • PageElements, modeling a collection of web elements and offering Page Element Query Language APIs (see section 15.2.6)

  • By选择器,提供一个通用的抽象,用于使用 CSS 或 XPath 查询来定位感兴趣的元素

  • By selector, providing a common abstraction around locating elements of interest using CSS or XPath queries

虽然 Serenity/JS 通过选择器实现,例如By.cssBy.deepCssBy.xpath, 或者By.id旨在为使用基于 Selenium WebDriver 的框架的开发人员提供熟悉的外观和感觉,它们中的每一个都与集成工具无关。这意味着无论您使用哪种集成工具,它们的工作方式都相同,因为 Serenity/JS 会在内部将选择器转换为等效的工具特定版本。

While Serenity/JS By selector implementations, such as By.css, By.deepCss, By.xpath, or By.id, are designed to provide a look and feel familiar to developers who used Selenium WebDriver–based frameworks, each one of them is integration tool agnostic. This means they work the same no matter the integration tool you use, since Serenity/JS translates the selectors internally to an equivalent tool-specific version.

要了解如何在实践中使用这些抽象,让我们考虑一下您已经熟悉的烤面包机小部件。成功注册后,烤面包机将显示一条消息,如图 15.3 所示。

To see how to use those abstractions in practice, let’s consider the toaster widget you’re already familiar with. Upon a successful registration, the toaster displays a message, as depicted in figure 15.3.

图 15.3 显示确认注册成功的消息的烤面包机小部件

Figure 15.3 The toaster widget displaying a message confirming successful registration

小部件生成的 HTML 结构(如清单 15.8 所示)与您在 15.1.6 节中熟悉的结构几乎相同。不过,此处小部件显示了一条成功消息,该消息通过容器元素上的 toast-success CSS 类来体现。

The HTML structure produced by the widget and shown in listing 15.8 is almost the same as the one you’re already familiar with from section 15.1.6. Here, however, the widget presents a success message, which manifests itself with a toast-success CSS class on the container element.

清单 15.8 烤面包机小部件

Listing 15.8 The toaster widget

<div class="ngx-toastr toast-success">
    <div class="toast-message">
        用户:alice@example.org 注册成功
    </div>
</div>
<div class="ngx-toastr toast-success">
    <div class="toast-message">
        User: alice@example.org registered successfully
    </div>
</div>

Serenity/JS 提供了两种主要方法来识别特定的 Web 元素,例如toast-message

Serenity/JS offers two main ways to identify a specific web element, such as toast-message.

第一种方法是使用By选择器相对于当前浏览上下文,例如浏览器选项卡或者其中的框架,例如:

The first method is to use a By selector relative to the current browsing context, such as a browser tab or a frame within it, for example:

从 '@serenity-js/web' 导入 { By, PageElement }
 
const ToasterMessage = () =>
    PageElement.located(By.css(`.ngx-toastr > .toast-message`))     
        .writtenAs('烤面包机消息')
import { By, PageElement } from '@serenity-js/web'
 
const ToasterMessage = () =>
    PageElement.located(By.css(`.ngx-toastr > .toast-message`))    
        .describedAs('toaster message')

❶CSS选择器识别元素。

CSS selector identifies the element.

第二种是描述页面元素与另一个包含元素的关系:

The second is to describe the page element in relation to another containing element:

从 '@serenity-js/web' 导入 { By, PageElement }
 
const Toaster = () =>                            
    PageElement.located(By.css(`.ngx-toastr`))         
        .describeAs(`烤面包机`)
 
const ToasterMessage = () =>
    PageElement.located(By.css(`.toast-message`))      
        .of(Toaster())                                 
        .writtenAs('消息')
import { By, PageElement } from '@serenity-js/web'
 
const Toaster = () =>                            
    PageElement.located(By.css(`.ngx-toastr`))        
        .describedAs(`toaster`)
 
const ToasterMessage = () =>
    PageElement.located(By.css(`.toast-message`))     
        .of(Toaster())                                
        .describedAs('message')

父页面元素

Parent page element

子页面元素

Child page element

建立子元素和父元素之间的关系

Establishes a relationship between the child element and the parent element

PageElement您可以同时使用这两种方法来定义对象链动态地或组合不同类型的选择器。当给定的 Web 元素可能在给定的页面上多次出现并且我们需要根据其包含的元素区分各种实例时,这尤其有用。链接PageElement对象的另一个用例当宿主元素仅在运行时才为人所知并且我们需要动态地注入它时:

You can use both those methods together to define a chain of PageElement objects dynamically or to combine different types of selectors. This can be particularly useful when a given web element can occur multiple times on a given page and we need to differentiate between the various instances based on their containing element. Another use case for chaining PageElement objects is when the host element only becomes known at runtime and we need to inject it dynamically:

从 '@serenity-js/web' 导入 { By, PageElement }
 
const Toaster = () =>                            
    页面元素.定位(By.css(`.ngx-toastr`))                           
        .describeAs(`烤面包机`)
 
const ToasterMessage = () =>
    页面元素.定位(By.css(`.toast-message`))                        
        .of(烤面包机())
        .writtenAs('消息')
 
 
const NotificationsSection = () =>
    PageElement.located(By.id(`通知`)     
      .writtenAs(`通知部分`)
 
 
 
const SpecificMessage = ToasterMessage()
    .of(NotificationsSection())                    
import { By, PageElement } from '@serenity-js/web'
 
const Toaster = () =>                            
    PageElement.located(By.css(`.ngx-toastr`))                           
        .describedAs(`toaster`)
 
const ToasterMessage = () =>
    PageElement.located(By.css(`.toast-message`))                        
        .of(Toaster())
        .describedAs(‘message')
 
 
const NotificationsSection = () =>
    PageElement.located(By.id(`notifications`)    
      .describedAs(`notifications section`)
 
 
 
const SpecificMessage = ToasterMessage()
    .of(NotificationsSection())                   

假设有一个包含烤面包机的通知部分

A hypothetical notification section hosting the toaster

根据动态注入的主机元素解析 Toaster 消息元素

A toaster message element resolved in relation to a dynamically injected host element

当然,单独定义每个页面元素很快就会变得相当笨重,所以让我们研究一下结构设计模式,以帮助我们将页面元素组织成表示小部件或集合的类。小部件。

Of course, defining each page element individually becomes rather unwieldy fairly quickly, so let’s look into structural design patterns to help us organize page elements into classes representing widgets or collections of widgets.

15.2.3 实现精益页面对象

15.2.3 Implementing Lean Page Objects

作为您已经从第 11 章(第 11.3 节)中了解了页面对象模式,经典的页面对象有两个职责:

As you already know from chapter 11 (section 11.3) where you learned about the Page Objects pattern, classic Page Objects have two responsibilities:

  • 它们提供了一种定位感兴趣的网络元素的方法。

  • They provide a way to locate web elements of interest.

  • 它们还模拟与给定网页或小部件相关的交互。

  • They also model interactions available with a given web page or widget.

由于 Serenity/JS 遵循您在第 12 章中学到的 Screenplay 模式,该框架鼓励您将重点放在与被测系统交互的参与者上,并使用任务和交互等概念来描述这些参与者如何实现其目标。这意味着代表这个世界中的 Web 小部件的对象具有简化的角色,Serenity/JS Lean Page Objects 无需同时建模结构和行为,而是只需负责提供有关小部件的信息。

Since Serenity/JS follows the Screenplay Pattern you learned about in chapter 12, the framework encourages you to place focus on the actors interacting with the system under test and uses concepts like tasks and interactions to describe how such actors go about accomplishing their goals. This means that objects representing web widgets in this world have a simplified role, and instead of modeling both the structure and behavior, Serenity/JS Lean Page Objects have a single responsibility of providing information about the widget.

以下清单显示了代表烤面包机小部件的精益页面对象的示例,您还可以在其中看到APIQuestionAdapter是如何生成的CssClasses.of(pageElement)允许我们将 CSS 类的低级概念转换为更有意义的“成功”或“错误”状态的概念小部件。

An example of a Lean Page Object representing the toaster widget is shown in the following listing, where you can also see how the QuestionAdapter produced by the CssClasses.of(pageElement) API allows us to transform the low-level concept of a CSS class into a much more meaningful concept of the “success” or “error” state of the widget.

清单 15.9 精益页面对象代表烤面包机小部件

Listing 15.9 Lean Page Object representing the toaster widget

从 '@serenity-js/web' 导入 {By、CssClasses、PageElement}
从 '@serenity-js/core' 导入 { QuestionAdapter }
 
导出类烤面包机{
    私有静态组件 = () =>
        页面元素.定位(By.css(`.ngx-toastr`))
            .describeAs('烤面包机')
    静态消息 = () =>
        页面元素.定位(By.css(`.toast-message`))
            .of(Toaster.component())                                      
            .writtenAs('消息')
 
    静态状态 = () =>
        CssClasses.of(Toaster.component())                                
            .filter(cssClass => cssClass.startsWith('toast-'))            
            .map(cssClass => cssClass.replace('toast-', ''))
            切片(0,1)[0]
            .writtenAs('烤面包机状态') as QuestionAdapter<string>     
}
import { By, CssClasses, PageElement } from '@serenity-js/web'
import { QuestionAdapter } from '@serenity-js/core'
 
export class Toaster {
    private static component = () =>
        PageElement.located(By.css(`.ngx-toastr`))
            .describedAs('toaster')
    static message = () =>
        PageElement.located(By.css(`.toast-message`))
            .of(Toaster.component())                                     
            .describedAs('message')
 
    static status = () =>
        CssClasses.of(Toaster.component())                               
            .filter(cssClass => cssClass.startsWith('toast-'))           
            .map(cssClass => cssClass.replace('toast-', ''))
            .slice(0, 1)[0]
            .describedAs('toaster status') as QuestionAdapter<string>    
}

❶PageElement是相对于另一个页面元素定位的。

PageElement is located relative to another page element.

QuestionAdapter 包装了 CSS 类的数组。

QuestionAdapter wraps an array of CSS classes.

对异步返回值的代理操作

Proxied operations on an asynchronously returned value

用于报告的自定义描述

Custom description to be used for reporting

15.2.4 实现配套页面对象

15.2.4 Implementing Companion Page Objects

甚至尽管 Screenplay 模式最初是作为 Page Objects 模式的替代方案提出的,3 但同时应用这两种模式的经验教训会很有益处;特别是在设计要与用户界面小部件库共享的测试代码时。

Even though the Screenplay Pattern was originally proposed as an alternative to the Page Objects Pattern,3 it can be beneficial to apply learnings from both patterns at the same time; particularly when designing test code to be shared with user interface widget libraries.

为了让开发人员更容易发现与给定小部件相关的测试代码,我们可以创建与小部件在同一模块中提供的测试专用配套页面对象。此类配套页面对象提供与小部件交互并返回其状态信息的测试方法。此外,我们可以重构清单 15.9 中的原始代码,以允许调用者注入主机元素,从而使我们的设计更加灵活。

To make it easier for developers to discover test code related to a given widget, we could create test-specific companion Page Objects shipped in the same module as the widget. Such companion Page Objects provide test methods that interact with the widget and return information about its state. Additionally, we could restructure the original code from listing 15.9 to allow for the host element to be injected by the caller to make our design more flexible.

此实现(如以下清单所示)与常规 Page Objects 类似。但重要的区别在于,伴随 Page Object 的所有方法都返回任务和问题适配器,以便于将其结果组合成更高级别的类型。

This implementation, as shown in the following listing, looks similar to regular Page Objects. The important difference, however, is that all the methods of the companion Page Object return tasks and question adapters to make it easy to compose their results into higher-level types.

清单 15.10 配套页面对象

Listing 15.10 Companion Page Objects

从 '@serenity-js/web' 导入 { By, Click, CssClasses, PageElement }
从 '@serenity-js/core' 导入 { QuestionAdapter, Task, Wait }
从 '@serenity-js/assertions' 导入 { isPresent, not }
 
导出类烤面包机{
  构造函数(私有只读 hostElement:QuestionAdapter <PageElement>){}
 
  消息 = () =>
    页面元素.定位(By.css(`.toast-message`))
      .of(this.hostElement).writtenAs('消息');
 
  状态 = () =>
    CssClasses.of(this.hostElement)
      .filter(cssClass => cssClass.startsWith('toast-'))
      .map(cssClass => cssClass.replace('toast-', ''))
      切片(0,1)[0]
      .writtenAs('烤面包机状态') 作为 QuestionAdapter<string>
 
  解除消息 = () =>
    Task.where(`#actor 关闭消息`,
      等待.直到(this.message(),isPresent()),
      点击(this.message()),
      等待.直到(this.message(),而不是(isPresent())),
}
import { By, Click, CssClasses, PageElement } from '@serenity-js/web'
import { QuestionAdapter, Task, Wait } from '@serenity-js/core'
import { isPresent, not } from '@serenity-js/assertions'
 
export class Toaster {
  constructor(private readonly hostElement: QuestionAdapter<PageElement>) {}
 
  message = () =>
    PageElement.located(By.css(`.toast-message`))
      .of(this.hostElement).describedAs('message');
 
  status = () =>
    CssClasses.of(this.hostElement)
      .filter(cssClass => cssClass.startsWith('toast-'))
      .map(cssClass => cssClass.replace('toast-', ''))
      .slice(0, 1)[0]
      .describedAs('toaster status') as QuestionAdapter<string>
 
  dismissMessage = () =>
    Task.where(`#actor dismisses the message`,
      Wait.until(this.message(), isPresent()),
      Click.on(this.message()),
      Wait.until(this.message(), not(isPresent())),
    )
}

15.2.5 实现与页面元素的可移植交互

15.2.5 Implementing portable interactions with Page Elements

作为Selenium WebDriver 的创建者 Simon Stewart 曾经说过:“如果你的测试方法中有 WebDriver API,那你就做错了。”他指出,在测试中直接调用 Web 界面集成工具的低级 API 会使这些测试变得脆弱且难以维护。

As Simon Stewart, creator of Selenium WebDriver, once said: “If you have WebDriver APIs in your test methods, you’re doing it wrong.” What he was pointing out is that directly invoking the low-level APIs of your web interface integration tool in your tests makes those tests brittle and difficult to maintain.

Simon 提出的解决方案是应用设计模式来帮助封装此类低级 API 调用并将其隐藏在领域友好的抽象后面;我们在第 11 章中讨论了几种这样的模式。Serenity/JS 将这种抽象低级 API 调用的想法更进一步,通过提供一组标准化的 Web 交互接口(同时仍允许访问低级 API,如果需要),您可以抽象整个 Web 界面集成工具。这在实践中意味着,Serenity/JS 充当较低级别集成工具(如 Selenium WebDriver、WebdriverIO、Playwright 等)的抽象层,您的测试可以不受这些工具之间细微和不太细微的差异的影响,同时保持与所有工具的兼容性。

Simon’s proposed solution was to apply design patterns that help to encapsulate such low-level API calls and hide them behind domain-friendly abstractions; we’ve looked at several such patterns in chapter 11. Serenity/JS takes this idea of abstracting the low-level API calls one step further, which enables you to abstract the entire web interface integration tool by providing a set of standardized web interaction interfaces (while still allowing access to the low-level APIs, if needed). What this means in practice is that with Serenity/JS acting as an abstraction layer around the lower-level integration tools, such as Selenium WebDriver, WebdriverIO, Playwright, and others, your tests can become agnostic of both the subtle and the not-so-subtle differences between those tools while remaining compatible with all of them.

为了实现这一点,Serenity/JS Web 交互模块 @serenity-js/web,提供了一组与 Screenplay 模式兼容的抽象,例如使您的参与者能够与被测系统交互的交互。这些交互包括Navigate.to(url)、、或等等Click.on(element)Scroll.to(element)Enter.the(value) .into(input)

To enable this, the Serenity/JS Web interaction module, @serenity-js/web, provides a set of Screenplay Pattern–compatible abstractions, such as the interactions that enable your actors to interact with the system under test. Those interactions include Navigate.to(url), Click.on(element), Scroll.to(element), or Enter.the(value) .into(input), among others.

Web 模块还提供了有关 、 或 的 Screenplay 问题Text.of(element)Value.of(input)CssClasses.of(element)交互一样,这些 API 提供了一种与集成工具无关的方式,可通过其 Web 界面获取有关系统状态的信息。

The web module also provides Screenplay questions about Text.of(element), Value.of(input), or CssClasses.of(element). Just like the interactions, those APIs provide an integration tool-agnostic way to get information about the state of the system through its web interface.

Serenity/JS 网络交互和问题设计用于通过PageElement对象进行功能组合您已在 15.2.2 节中了解过。例如,要单击烤面包机消息元素,我们将Click使用表示烤面包机消息的页面元素来编写交互:

Serenity/JS web interactions and questions are designed for functional composition with PageElement objects you’ve learned about in section 15.2.2. For example, to click on the toaster message element, we’d compose the interaction to Click with a page element representing the toaster message:

点击(Toaster.message())
Click.on(Toaster.message())

为了从清单 15.9 和 15.10 中检索文本Toaster.message(),我们将有关文本的问题与有关页面元素的问题组合在一起:

To retrieve the text of the Toaster.message() from listings 15.9 and 15.10, we’d compose the question about text with one about the page element:

文本.of(Toaster.message())
Text.of(Toaster.message())

请注意,就像PageElementAPI返回一个QuestionAdapter<PageElement>Text.ofAPI返回一个QuestionAdapter<string>,都与验证任务兼容确保:

Note that just like PageElement API returns a QuestionAdapter<PageElement>, Text.of API returns a QuestionAdapter<string>, both compatible with the verification task to Ensure:

确保(Toaster.message(),isPresent())
确保(Text.of(Toaster.message()),包括('已注册 
成功'))
Ensure.that(Toaster.message(), isPresent())
Ensure.that(Text.of(Toaster.message()), includes('registered 
 successfully'))

15.2.6 使用页面元素查询语言描述复杂的 UI 小部件

15.2.6 Using Page Element Query Language to describe complex UI widgets

与自动化与复杂 Web 界面交互的测试特别相关的挑战之一是找到正确的选择器,使我们能够唯一地标识感兴趣的可交互元素,理想情况下,我们不必随着 Web UI 的每次细微变化而更改该元素。正如您在第 10 章中看到的,Selenium 等工具提供了许多基本的定位器策略,例如By.idBy.cssBy.xpath。由于 Serenity/JS Web 模块是围绕 Selenium WebDriver、WebdriverIO、Playwright 和其他 Web 集成工具的抽象层,因此它也支持这些基本的定位器策略。除此之外,它还提供了一种与集成工具无关的页面元素查询语言,这在自动与更复杂的 Web 界面进行交互时很有用。

One of the challenges particularly related to automating tests interacting with complex web interfaces is finding the right selector to allow us to uniquely identify the interactable element of interest, ideally one that we don’t have to change with every slightest change of the web UI. As you already saw in chapter 10, tools like Selenium provide a number of basic locator strategies such as By.id, By.css, or By.xpath. Since Serenity/JS Web module is an abstraction layer around Selenium WebDriver, WebdriverIO, Playwright, and other web integration tools, it supports those basic locator strategies as well. Apart from that, it also provides an integration tool-agnostic Page Element Query Language that is useful when automating interactions with the more sophisticated web interfaces.

为了更清楚地理解这一点,请考虑 Flying High Airlines 应用的注册表单,该应用使用流行的 Angular Material 框架(https://material.angular.io/)构建,如图 15.4 所示。

To put this in context, consider the registration form of the Flying High Airlines app, built using the popular Angular Material framework (https://material.angular.io/) and shown in figure 15.4.

图15.4 Flying High 账户注册表

Figure 15.4 Flying High account registration form

让我们假设我们的 Web UI 不提供任何易于测试的 HTML 标识符(例如data-test-id属性),而是需要根据其标签的文本查找输入元素。虽然理想情况下,您应该能够更改被测系统的界面以添加这些友好的测试标识符,但这可能并不总是可行或理想的。

Let’s pretend for a moment that our web UI doesn’t offer any test-friendly HTML identifiers (such as the data-test-id attributes), and instead we need to find an input element based on the text of its label. While ideally you should be able to alter the interface of the system under test to add those friendly test identifiers, this might not always be possible or desirable.

为了更好地理解页面元素查询语言如何帮助我们完成这项任务,让我们首先看一下图 15.4 中的注册表单的一个表单字段小部件(见图 15.5)。

To better understand how the Page Element Query Language could help us with this task, let’s first look at one of the form field widgets of the registration form from figure 15.4 (see figure 15.5).

图 15.5 Flying High 帐户注册表单字段小部件

Figure 15.5 Flying High account registration form field widget

该小部件生成的 HTML 结构如图 15.5 所示,您可以在 Web 浏览器的开发者工具中检查该结构(使用 Chrome DevTools 检查元素:https://developer.chrome.com/docs/devtools/open/),如下所示(示例中不相关的一些 CSS 类已被删除):

The HTML structure generated for this widget, rendered in figure 15.5 and that you can inspect in your web browser developer tools (using Chrome DevTools to inspect an element: https://developer.chrome.com/docs/devtools/open/), looks as follows (some of the CSS classes not relevant in the example were removed):

<mat-form-field>
    <div class="mat-form-field-wrapper">
        <div class="mat-form-field-flex">
            <div class="mat-form-field-infix">
                <输入名称=”电子邮件”>
                <跨度>
                    <label for="电子邮件" aria-owns="电子邮件">
                        <mat-label>电子邮件</mat-label>
                    </标签>
                </span>
            </div>
        </div>
        <div>
            <mat-error>请输入您的电子邮件</mat-error>
        </div>
    </div>
</mat-form-field>
<mat-form-field>
    <div class="mat-form-field-wrapper">
        <div class="mat-form-field-flex">
            <div class="mat-form-field-infix">
                <input name=”email”>
                <span>
                    <label for="email" aria-owns="email">
                        <mat-label>Email</mat-label>
                    </label>
                </span>
            </div>
        </div>
        <div>
            <mat-error>Please enter your email</mat-error>
        </div>
    </div>
</mat-form-field>

由于我们想要通过元素中显示的文本来查找输入字段<label>,因此我们首先定义如何识别表单字段。在我们的示例中,使用 Angular Material 生成的表单字段小部件使用名为的自定义 HTML 标签mat-form-field

Since we want to find an input field by text displayed in its <label> element, we first define how to identify form fields in general. In our example, a form field widget generated with angular material uses a custom HTML tag called mat-form-field.

我们定义表单字段作为具有此标签名称的所有页面元素,使用PageElementsAPI您首先在第 15.2.2 节中遇到的:

We define form fields as all the page elements with this tag name, using the PageElements API you first encountered in section 15.2.2:

从 '@serenity-js/web' 导入 { PageElements, By }
 
类形式{
    公共静态字段 = () =>
        页面元素.定位(By.css('mat-form-field'))
            .writtenAs('表单字段')
    // ...
}
import { PageElements, By } from '@serenity-js/web'
 
class Form {
    public static fields = () =>
        PageElements.located(By.css('mat-form-field'))
            .describedAs('form fields')
    // ...
}

接下来,我们描述如何识别单个标签,Angular Material UI 使用另一个自定义 HTML 标签来呈现该标签,mat-label

Next, we describe how to identify an individual label, which the angular material UI renders using another custom HTML tag, mat-label:

从 '@serenity-js/web' 导入 { PageElement, By }
 
类形式{
    公共静态标签 = () =>
        页面元素.定位(By.css('mat-label'))
            .describeAs('标签')
    // ...
}
import { PageElement, By } from '@serenity-js/web'
 
class Form {
    public static label = () =>
        PageElement.located(By.css('mat-label'))
            .describedAs('label')
    // ...
}

这两个构建块足以让我们定义一个方法,Form.fieldCalled(name),用于查找具有所需标签文本的表单字段。此方法使用页面元素查询语言,它依赖于您用于定义断言、同步和流控制子句的相同期望,并且您可以使用自己的期望和问题对其进行扩展:

Those two building blocks are enough for us to define a method, Form.fieldCalled(name), that finds a form field with the desired label text. This method uses the Page Element Query Language, which relies on the same expectations you use to define assertion, synchronization, and flow-control clauses, and which you can extend with your own expectations and questions:

从 '@serenity-js/web' 导入 { Text }
从 '@serenity-js/assertions' 导入 { isPresent, equals }
 
类形式{
    私有静态 fieldCalled = (名称:字符串) =>
        表单.字段()
            .where(Form.label(),isPresent())               
            .where(Text.of(Form.label()), equals(name))     
            .first()                                        
    // ...
}
import { Text } from '@serenity-js/web'
import { isPresent, equals } from '@serenity-js/assertions'
 
class Form {
    private static fieldCalled = (name: string) =>
        Form.fields()
            .where(Form.label(), isPresent())              
            .where(Text.of(Form.label()), equals(name))    
            .first()                                       
    // ...
}

查找带有标签的字段。

Find fields with a label.

查找标签包含所需文本的字段。

Find fields where the label has the desired text.

返回第一个符合我们期望的元素。

Return the first element that meets our expectations.

当识别出正确的字段时,引用其<input>元素就变得很简单,因为我们可以使用.ofAPI您从第 15.2.2 节了解:

When the correct field is identified, referencing its <input> element becomes trivial, since we can use the .of API you know from section 15.2.2:

类形式{
    // ...
    静态 inputFor = (名称:字符串) =>
        表单.输入()
            .of(表单.fieldCalled(名称))
            .writtenAs(`“${ name }” 字段`)
}
class Form {
    // ...
    static inputFor = (name: string) =>
        Form.input()
            .of(Form.fieldCalled(name))
            .describedAs(`the "${ name }" field`)
}

Form班级一起提供一种简单的方法来识别感兴趣的领域,诸如您之前看到的任务SpecifyEmailAdress就变成了简单的一行代码:

And with the Form class providing an easy way to identify the fields of interest, tasks such as the one to SpecifyEmailAdress you saw earlier become simple one-liners:

导出 const SpecifyEmailAddress = (emailAddress:Answerable<string>)=>
    Task.where(`#actor 指定他们的电子邮件地址`,
        输入.theValue(emailAddress).into(Form.inputFor('Email')))
export const SpecifyEmailAddress = (emailAddress: Answerable<string>) =>
    Task.where(`#actor specifies their email address`,
        Enter.theValue(emailAddress).into(Form.inputFor('Email')))

Form 类的完整实现如下面的清单所示。

The completed implementation of the Form class is shown in the following listing.

清单 15.11 使用页面元素查询语言完成的表单类

Listing 15.11 Completed Form class using Page Element Query Language

从'@serenity-js/assertions'
导入 { matches, includes, isPresent }'@serenity-js/web'
导入{ By, PageElement, PageElements, Text } 
导出类表单{
    静态buttonCalled = (名称:字符串) =>
        表单.按钮()
            .where(文本,包括(名称))
            。第一的()
            .writtenAs(`“${ name }”按钮`)
 
    静态输入 = (名称:字符串) =>
        表单.输入()
            .of(表单.fieldCalled(名称))
            .writtenAs(`“${ name }” 字段`)
 
    静态errorMessageFor =(名称:字符串)=>
        文本.的(
            表单.errorMessage()
                .of(表单.fieldCalled(名称))
                .writtenAs(`“${ name }”字段的错误消息`)
 
    私有静态fieldCalled = (名称:字符串) =>
        表单.字段()
            .其中(表单.标签(),isPresent())
            .where(Text. of (Form.label()), 匹配( new RegExp(name, 'i')))
            。第一的()
 
    公共静态按钮 = () =>
        PageElements.位于(By.css  'button'))
            .describeAs('按钮');
 
    公共静态字段 = () =>
        PageElements.l ocated(By.css('mat-form-field'))
            .writtenAs('表单字段');
 
    公共静态标签 = () =>
        PageElement.位于(By.css  'label > mat-label,label > span'))
            .describeAs('标签')
 
    私有静态输入 = () =>
        PageElement.位于(By.css  'input'))
 
    私有静态错误消息 = () =>
        PageElement.located (By.css ( 'mat - error'));
}
import { matches, includes, isPresent } from '@serenity-js/assertions'
import { By, PageElement, PageElements, Text } from '@serenity-js/web'
 
export class Form {
    static buttonCalled = (name: string) =>
        Form.buttons()
            .where(Text, includes(name))
            .first()
            .describedAs(`the "${ name }" button`)
 
    static inputFor = (name: string) =>
        Form.input()
            .of(Form.fieldCalled(name))
            .describedAs(`the "${ name }" field`)
 
    static errorMessageFor = (name: string) =>
        Text.of(
            Form.errorMessage()
                .of(Form.fieldCalled(name))
                .describedAs(`the error message for "${ name }" field`)
        )
 
    private static fieldCalled = (name: string) =>
        Form.fields()
            .where(Form.label(), isPresent())
            .where(Text.of(Form.label()), matches(new RegExp(name, 'i')))
            .first()
 
    public static buttons = () =>
        PageElements.located(By.css('button'))
            .describedAs('buttons');
 
    public static fields = () =>
        PageElements.located(By.css('mat-form-field'))
            .describedAs('form fields');
 
    public static label = () =>
        PageElement.located(By.css('label > mat-label, label > span'))
            .describedAs('label')
 
    private static input = () =>
        PageElement.located(By.css('input'))
 
    private static errorMessage = () =>
        PageElement.located(By.css('mat-error'));
}

现在您已经了解了如何识别页面元素,使用 Lean 和 Companion Page Objects 等模式将它们组织成更有意义的结构,以及如何使用 Serenity/JS API 与它们进行交互,让我们更深入地了解 Serenity/JS 与 Playwright 或 WebdriverIO 等测试集成工具之间的集成如何在兜帽。

Now that you know how to identify page elements, organize them into more meaningful structures using patterns like Lean and Companion Page Objects, and interact with them using Serenity/JS APIs, let’s dive a bit deeper to understand how the integration between Serenity/JS and test integration tools like Playwright or WebdriverIO work under the hood.

15.2.7 配置 Web 集成工具

15.2.7 Configuring web integration tools

作为你已经知道,@serenity-js/web模块提供 Screenplay Pattern API,允许参与者与被测系统的 Web 界面进行交互。但是,模块本身不依赖于任何测试集成工具,也不直接与任何测试集成工具(如 Playwright、Selenium WebDriver、WebdriverIO 等)交互。相反,它遵循服务提供商框架架构。4意味着它依赖于其他 Serenity/JS 模块,例如 @serenity-js/playwright或 @serenity-js/webdriverio,提供以下概念的实现Page或者PageElement并与各自的测试工具集成。

As you already know, the @serenity-js/web module provides Screenplay Pattern APIs, allowing the actors to interact with the web interface of the system under test. The module itself, however, does not depend on, nor interact directly with, any test integration tools such as Playwright, Selenium WebDriver, WebdriverIO, and so on. Instead, it follows the service provider framework architecture.4 This means that it relies on other Serenity/JS modules, such as @serenity-js/playwright or @serenity-js/webdriverio, to provide implementations of concepts such as Page or PageElement, and to integrate with their respective test tools.

但是,你不会调用如下类WebdriverIOPage或者PlaywrightPageElement直接在测试中使用这些模块提供的服务。相反,您可以使用以下服务访问 API:Page.current()或者PageElement.located(By.css('.selector'))获取适合您当前执行上下文的给定服务类的实例。此机制使您基于 Serenity/JS 的测试和测试自动化系统能够与底层测试集成工具无关,并可在各种实现之间移植。

However, you wouldn’t invoke classes like WebdriverIOPage or PlaywrightPageElement provided by those modules directly in your tests. Instead, you’d use service access APIs like Page.current() or PageElement.located(By.css('.selector')) to obtain an instance of a given service class appropriate to your current execution context. This mechanism is what allows your tests and test automation systems based on Serenity/JS to be agnostic of the underlying test integration tool and portable across the various implementations.

但是,如果所有测试代码都与测试集成工具无关,Serenity/JS 如何知道要使用哪种测试集成工具?有一部分是与测试集成工具无关的,即特定于工具的实现,以BrowseTheWeb, 例如BrowseTheWebWithWebdriverIO或者BrowseTheWebWithPlaywright,您第一次遇到的是第 14 章(第 14.2.2 节)。此工具特定的实现在您配置CastSerenity/JS 参与者负责实例化适当版本的服务类,例如Page或者PageElement,与测试集成工具交互,并提供一致的API:

But how does Serenity/JS know what test integration tool to use if all your test code is test integration tool agnostic? There’s one part that isn’t, and that part is the tool-specific implementation of the ability to BrowseTheWeb, such as BrowseTheWebWithWebdriverIO or BrowseTheWebWithPlaywright, that you first encountered in chapter 14 (section 14.2.2). This tool-specific implementation provided when you configure your Cast of Serenity/JS actors is what’s responsible for instantiating an appropriate version of service classes such as Page or PageElement, interacting with the test integration tool, and providing a consistent API:

'@serenity-js/core'
导入{ actorCalled, configure, Cast }'@serenity-js/playwright'
导入{ BrowseTheWebWithPlaywright }‘playwright’
导入*作为剧作家 
const browser: playwright.Browser = await playwright.chromium.launch()    
    
配置({
  演员:Cast.whereEveryoneCan(                               
    BrowseTheWebWithPlaywright.using(浏览器)                             
})
 
actorCalled('William').attemptsTo(
  Click.on(PageElement.located(By.css('.selector')),                      
import { actorCalled, configure, Cast } from '@serenity-js/core'
import { BrowseTheWebWithPlaywright } from '@serenity-js/playwright'
import * as playwright from 'playwright'
 
const browser: playwright.Browser = await playwright.chromium.launch()   
    
configure({
  actors: Cast.whereEveryoneCan(                               
    BrowseTheWebWithPlaywright.using(browser)                            
  ) 
})
 
actorCalled(‘William’).attemptsTo(
  Click.on(PageElement.located(By.css(‘.selector’)),                     
)

获取测试集成工具实例

Obtains an instance of the test integration tool

使用特定于工具的 BrowseTheWeb 功能

Uses a tool-specific version of the ability to BrowseTheWeb

与工具无关的交互和 PageElement

Tool-agnostic interaction and PageElement

要亲自了解 Serenity/JS Web 模块及其相关的特定于工具的插件如何使相同的测试代码能够同时在 WebdriverIO 和 Playwright 中执行,请探索此代码存储库章。

To see for yourself how Serenity/JS Web module and its associated tool-specific plug-ins enable the same test code to be executed with both WebdriverIO and Playwright, explore the code repository for this chapter.

15.2.8 跨项目和团队共享测试代码

15.2.8 Sharing test code across projects and teams

一致的 Screenplay Pattern API,对 JavaScript 的异步特性提供一流的支持,以及与测试集成工具无关的编程模型,允许基于 Serenity/JS 的测试自动化代码在软件系统可能拥有的各种测试套件中轻松重用。然而,这些一致性、可移植性和可预测性的特性也可以帮助您解决常见的组织问题。

The consistent Screenplay Pattern APIs, with first-class support for the asynchronous nature of JavaScript, as well as a test integration tool-agnostic programming model, allow for the test automation code based on Serenity/JS to be easily reused across the various test suites a software system might have. However, those characteristics of consistency, portability, and predictability can help you solve a common organizational problem too.

更具体地说,从事大型项目的软件交付组织的一个常见模式是让专门的团队负责开发共享组件。这些共享组件(例如 Web UI 小部件或 REST API)随后被消费者团队用作构建块,组装成软件系统。这种组织模式在构建基于仪表板的应用程序的公司中很普遍,这些公司有中央组件团队来制作 UI 小部件并支持后端服务,以确保所有此类仪表板的外观和感觉一致。

More specifically, a common model for software delivery organizations working on large-scale projects is to have dedicated teams responsible for developing shared components. Those shared components, such as the web UI widgets or REST APIs, are then used by consumer teams as building blocks to be assembled into software systems. This organizational pattern is prevalent, for example, among companies that build dashboard-based apps and have central component teams producing UI widgets and supporting backend services to ensure a consistent look and feel across all such dashboards.

然而,这种模式的一个常见问题是,即使组件团队可能已经将其工作产品的测试自动化到最高标准,消费者团队在编写与此类共享组件交互的端到端测试时通常不会从中受益。此外,消费者团队通常需要做大量额外的工作,因为他们必须自己发现如何让他们的测试与共享组件一起工作,或者使用什么选择器来定位 Web UI 小部件的有趣元素。更糟糕的是,消费者团队需要让他们的高级测试与组件团队对其 UI 小部件或支持服务的结构或行为引入的任何更改保持同步。

A common problem in this model, however, is that even though the component teams might have automated the testing of their work product to the highest possible standard, consumer teams typically don’t benefit from that at all when writing their end-to-end tests interacting with such shared components. Furthermore, consumer teams typically end up with a substantial amount of additional work as they must discover for themselves how to make their tests work with the shared components or what selectors to use to locate the interesting elements of a web UI widget. What’s worse, consumer teams need to keep their higher-level tests in sync with any changes introduced by the component teams to the structure or behavior of their UI widgets or supporting services.

这里的问题不仅仅是代码和工作的重复。当组件团队对共享组件进行更改时,还存在着在不知情的情况下破坏其他团队测试的固有风险。这种情况通常发生在 UI 组件团队更改其组件的 HTML 结构,或者 API 团队更改其 API 支持的某些数据结构的形状时。虽然您可以通过契约测试在一定程度上缓解后者,但重复代码和工作的问题仍然存在。

The problem here is not just the duplication of code and effort. There’s also the inherent risk of component teams unknowingly breaking other teams’ tests when they introduce changes to the shared components. This typically happens when a UI component team changes the HTML structure of their component, or an API team changes the shape of some data structure supported by their API. While you could mitigate the latter to a degree with contract tests, the problem of duplicated code and effort remains.

假设参与的团队习惯使用 JavaScript 兼容语言(如 TypeScript)来实现组件级和更高级别的测试,那么编程模型和代码重用模式(如 Companion Page Objects 和 Screenplay 任务)可以为这个问题提供解决方案。要在我们的场景中使用它,组件团队需要发布他们已经用于独立测试组件的基于 Serenity/JS 的测试代码,并让消费者团队可以轻松地将此类测试代码与适合的组件版本关联起来。一种适用于共享 UI 小部件相关测试代码的机制是将其作为用于运送小部件的同一 Node 模块的一部分发布,但使用单独的入口点,以避免共享测试代码与生产代码。

Assuming the teams involved are comfortable with using a JavaScript-compatible language (like TypeScript) to implement both the component-level and higher-level tests, the programming model and code reuse patterns like Companion Page Objects and Screenplay tasks can offer a solution to this problem. To use it in our scenario, component teams would need to publish the Serenity/JS-based test code they’ve already used to test the components independently and make it easy for the consumer teams to associate such test code with the version of the component it is appropriate for. A mechanism that works well for sharing UI widget-related test code is to publish it as part of the same Node module used to ship the widgets, but under a separate entry point to avoid interference of shared test code with production code.

概括

Summary

  • 在自动化验收测试中,被测系统的外部接口是测试自动化系统与业务逻辑交互的手段,而不是重点。

  • In automated acceptance testing, external interfaces of the system under test are the means for a test automation system to interact with the business logic, not the focus.

  • 任务是子任务或交互的集合。

  • Tasks are aggregates of subtasks or interactions.

  • 任务参数使得任务的后果可以发生变化。

  • Task parameters enable variations in the consequences of a task.

  • 任务参数是任务参数的具体值(例如,如果旅行者详细信息是任务参数,则传递给任务的有效旅行者详细信息的具体示例就是任务参数)。

  • A task argument is the concrete value of a task parameter (e.g., if traveler details are a task parameter, a concrete example of valid traveler details passed to a task is a task argument).

  • 交互是低级活动,直接调用参与者能力的方法。

  • Interactions are low-level activities, directly invoking methods on actor’s abilities.

  • 由外而内的方法可以实现任务替换,用一项任务替代另一项能够实现相同目标的任务。

  • The outside-in approach enables task substitution, substituting one task for another that accomplishes the same goal.

  • 混合测试是一种有助于在以 UI 为中心的工作流测试中利用非 UI 交互的方法。

  • Blended testing is an approach that helps leverage non-UI interactions in otherwise UI-focused workflow tests.

  • Serenity/JS 页面元素查询语言为低级元素选择器(如 XPath)提供了一种可重用且可移植的替代方案。

  • Serenity/JS Page Element Query Language offers a reusable and portable alternative to low-level element selectors, like XPath.

  • Serenity/JS Lean Page Objects 定义了 Web UI 小部件的难以处理的元素及其描述。

  • Serenity/JS Lean Page Objects define the intractable elements of a web UI widget, together with their descriptions.

  • 配套页面对象是页面对象模式的 Screenplay 模式风格变体,旨在支持 Web UI 小部件库的跨项目测试代码重用。

  • Companion Page Objects are a Screenplay Pattern–style variation of the Page Objects pattern, designed to support cross-project test code reuse for Web UI widget libraries.

  • @serenity-js/web 模块遵循的服务提供商框架架构使基于 Serenity/JS 的测试自动化系统与底层测试集成工具(如 Playwright 或 WebdriverIO)无关。

  • The service provider framework architecture followed by the @serenity-js/web module enables your test automation systems based on Serenity/JS to be agnostic of the underlying test integration tool, like Playwright or WebdriverIO.

  • 所有 Serenity/JS API 都与原生 JavaScript Promises 兼容,并为该语言的异步特性提供一流的支持。

  • All Serenity/JS APIs are compatible with native JavaScript Promises and offer a first-class support for the asynchronous nature of the language.

  • Serenity/JS 提供了一致且易于学习的编程模型,您可以将其用作基础,实现跨项目和团队的代码重用。

  • Serenity/JS offers a consistent and easy-to-learn programming model, you can use as a foundation to enable code reuse across projects and teams.

  • 学习 Serenity/JS 的最好方法就是尝试一下!使用本章存储库中的示例测试自动化系统进行实验,以学习更多的。

  • The best way to learn Serenity/JS is to try it out! Experiment with the example test automation system in this chapter’s repository to learn more.


1  Eric Evans,领域驱动设计:解决软件核心的复杂性(Addison-Wesley Professional,2003),第 32-35 页。

1  Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (Addison-Wesley Professional, 2003), pp. 32–35.

2  广受欢迎的 Angular Protractor 就是这种情况,它于 2020 年收到最后一次提交,并随着 Angular v16 正式终止生命周期,预计将于 2023 年夏天终止:https://blog.angular.io/the-state-of-end-to-end-testing-with-angular-d175f751cb9c

2  This was the case with the hugely popular Angular Protractor, which received its last commit back in 2020 and reached an official end-of-life with Angular v16, expected in the summer of 2023: https://blog.angular.io/the-state-of-end-to-end-testing-with-angular-d175f751cb9c.

3  Antony Marcano、Andy Palmer、John Ferguson Smart 和 Jan Molak。“页面对象重构:剧本模式的 SOLID 步骤”2016 年,https://dzone.com/articles/page-objects-refactored-solid-steps-to-the-screenp

3  Antony Marcano, Andy Palmer, John Ferguson Smart, and Jan Molak. “Page Objects Refactored: SOLID Steps to the Screenplay Pattern” 2016, https://dzone.com/articles/page-objects-refactored-solid-steps-to-the-screenp.

4  Joshua Bloch,《Effective Java》,第二版(Addison-Wesley,2008)。

4  Joshua Bloch, Effective Java, 2nd edition (Addison-Wesley, 2008).

16 生活记录和释放证据

16 Living documentation and release evidence

本章封面

This chapter covers

  • 我们所说的“动态文档”
  • What we mean by “living documentation”
  • 使用功能就绪性和功能覆盖率来跟踪项目进度
  • Keeping track of project progress using feature readiness and feature coverage
  • 整理你的生活文档
  • Organizing your living documentation
  • 技术动态文档
  • Technical living documentation

在本章中,我们将重点介绍 BDD 的一个重要部分,如果您想要充分利用所采用的任何 BDD 策略,就需要了解这个部分。您已经了解了 BDD 如何鼓励团队以可执行规范的形式表达需求,而这些规范可以以自动化测试的形式运行。这些可执行规范成为当前应用程序需求集的权威参考(通常称为“事实来源”)。这些可执行规范的权威形式通常是源代码,因此它们可以很好地融入整个开发过程并驱动自动化测试。自动化测试生成的报告引用原始可执行规范。这些报告结合了原始规范、验收标准和测试结果,就是我们所说的活文档

In this chapter, we focus on an important part of BDD that you need to understand if you’re to get the most out of whatever BDD strategy you adopt. You’ve seen how BDD encourages teams to express requirements in terms of executable specifications that can be run in the form of automated tests. These executable specifications become the definitive reference (often referred to as the “source of truth”) for the current set of application requirements. The definitive form of these executable specifications is generally source code, so they fit neatly into the overall development process and drive the automated tests. The reports generated by the automated tests refer to the original executable specifications. These reports, which combine the original specifications, acceptance criteria, and test results, are what we call living documentation.

动态文档是 BDD 流程的关键部分。业务利益相关者可以使用它来审查某个功能的作用,并确认它是否符合业务需求和约束。测试人员可以将其用作探索性测试的起点,从而节省日常手动测试基本功能的时间。开发人员可以使用它来了解现有功能的作用及其工作原理。

Living documentation is a key part of the BDD process. Business stakeholders can use it to review what a feature does and confirm that it corresponds to business needs and constraints. Testers can use it as a starting point for exploratory testing, saving time in routine manual testing of basic features. And developers can use it to understand what an existing feature does and how it works.

许多组织还使用作为 BDD 流程的一部分生成的业务可读测试报告来记录特定版本中包含哪些功能、它们与哪些业务规则相关以及如何测试它们。这可能只是为了与利益相关者沟通,也可能是出于审计或合规目的。我们通常将为此目的生成的动态文档报告称为已发布证据。在本章中,您将学习如何生成和组织有效的生活文档,并了解一些可用于简化您自己的项目的工具和技术。

Many organizations also use the business-readable test reports produced as part of a BDD process to document what features go into a particular release, what business rules they relate to, and how they were tested. This may simply be for communication with stakeholders or might be required for audit or compliance purposes. We often call living documentation reports produced for this purpose released evidence. In this chapter you will learn how to generate and organize effective living documentation and see some of the tools and techniques you can use to streamline your own projects.

16.1 动态文档:高层视图

16.1 Living documentation: A high-level view

身体缺陷诊断报告不仅仅提供测试结果列表(即测试通过或失败)。首先,BDD 报告记录并描述应用程序预期要执行的操作,并报告应用程序是否正确执行了这些操作。当您深入了解详细信息时,BDD 报告还会从用户的角度说明特定特性或功能的执行方式。

BDD reports don’t simply provide a list of test outcomes, in terms of passing or failing tests. First, BDD reports document and describe what the application is expected to do, and they report whether the application performs these operations correctly. When you drill into the details, a BDD report also illustrates how a particular feature or functionality is performed, from the user’s perspective.

动态文档是始终保持最新状态并始终反映系统当前状态的文档。当我们与客户或产品所有者协作编写功能文件时,我们正在创建可执行规范。当我们使用 Cucumber 等工具自动执行这些规范时,它们就变成了动态文档。动态文档这个术语可以指功能文件本身(假设它们是自动执行的)或执行测试时生成的测试报告。

A living document is a document that is always up to date and that always reflects the current state of the system. When we write feature files in collaboration with customers or product owners, we are creating executable specifications. When we automate these specifications using tools like Cucumber, they become living documentation. The term living documentation can refer to the feature files themselves (assuming they are automated and executed) or the test reports that are generated when the tests are executed.

动态文档面向广泛的受众(见图 16.1)。您已经了解了 BDD 如何鼓励团队协作以具体示例、场景和可执行规范的形式定义验收标准,以及这些标准如何指导正在构建的功能的开发和交付。在交付功能时,动态文档会将功能与原始需求联系起来,确认交付的内容与团队最初讨论的内容相符。

Living documentation targets a broad audience (see figure 16.1). You’ve seen how BDD encourages teams to collaborate to define acceptance criteria in the form of concrete examples, scenarios, and executable specifications, and how these guide the development and delivery of the features being built. As features are delivered, the living documentation ties the features back to the original requirements, confirming that what was delivered corresponds to what the team originally discussed.

图 16.1 动态文档为整个团队提供反馈,特别是业务分析师、测试人员和业务利益相关者。动态文档可以引用执行时的功能文件以及执行后生成的报告。

Figure 16.1 Living documentation provides feedback to the whole team, in particular to business analysts, testers, and business stakeholders. Living documentation can refer to feature files when they are executed and to the reports that are generated as a result of this execution.

这样,BDD 报告就完成了从与业务利益相关者的初始对话开始的循环。利益相关者、业务分析师、测试人员以及参与场景和可执行规范对话的其他任何人都会看到他们进行的对话以及他们讨论的示例作为生成的报告的一部分出现。这种反馈循环是获得业务利益相关者认可的好方法,当他们在动态文档中看到他们贡献的结果时,他们往往更愿意积极做出贡献。此外,由于报告是根据自动验收标准自动生成的,因此一旦设置好,它就是一种快速有效的提供反馈的方式(见图 16.2)。

In this way, BDD reporting completes the circle that started with the initial conversations with business stakeholders. The stakeholders, business analysts, testers, and anyone else who participated in the conversations leading up to the scenarios and executable specifications see the conversations they had and the examples they discussed appear as part of the generated reports. This feedback loop is a great way to get buy-in from business stakeholders, who are often much more keen to contribute actively when they see the results of their contributions verbatim in the living documentation. In addition, because the reports are generated automatically from the automated acceptance criteria, it’s a fast and efficient way of providing feedback once it’s set up (see figure 16.2).

图 16.2 动态文档是根据可执行规范自动生成的,从而简化了报告并加快了反馈周期。

Figure 16.2 Living documentation is generated automatically from the executable specifications, which simplifies reporting and accelerates the feedback cycle.

测试人员还使用动态文档来补充他们自己的测试活动,以了解功能的实现方式,并更好地了解他们应该重点进行探索性测试的领域。

Testers also use the living documentation to complement their own testing activities, to understand how features have been implemented, and to get a better idea of the areas in which they should focus their exploratory testing.

动态文档的好处不应该在项目交付后就结束。如果组织得当,动态文档也是让新团队成员快速了解应用程序应该做什么以及它如何工作的好方法。对于在项目投入生产后将其移交给其他团队的组织来说,仅此一项好处就值得花时间设置动态文档。

The benefits of living documentation shouldn’t end when a project is delivered. When organized appropriately, living documentation is also a great way to bring new team members up to speed not only with what the application is supposed to do, but also how it does it. For organizations that hand over projects to a different team once they go into production, the benefits of this alone can be worth the time invested in setting up the living documentation.

但动态文档不仅仅是描述和说明已构建的功能。许多团队还将他们的 BDD 报告与 Agile 项目管理或问题跟踪系统集成,从而可以专注于特定版本计划的功能状态。如果发布报告不是从 BDD 报告中自动生成的,那么这样做的团队通常依靠动态文档来生成发布报告。

But living documentation goes beyond describing and illustrating the features that have been built. Many teams also integrate their BDD reports with Agile project management or issue-tracking systems, making it possible to focus on the state of the features planned for a particular release. Teams that do this typically rely on the living documentation to produce their release reports if they aren’t generated automatically from the BDD reports.

在本章的其余部分,我们将更详细地介绍动态文档的一些不同方面。我将使用 Serenity 报告和其他一些类似工具来说明许多原则,但这些原则并不特定于任何特定工具放。

In the rest of this chapter, we’ll look at some of these different aspects of living documentation in more detail. I’ll illustrate many of the principles using Serenity reports and a few other similar tools, but the principles aren’t specific to any particular tool set.

16.2 报告功能准备情况和功能覆盖率

16.2 Reporting on feature readiness and feature coverage

BDD 中的一个核心概念就是我们所说的功能就绪性。从这个意义上讲,特性只是利益相关者关心的功能片段。有些团队使用用户故事来扮演这个角色,而其他团队则倾向于将用户故事与更高级别的特性区分开来。这两种情况下的原则是相同的:当开发团队报告进度时,利益相关者对哪些单个测试通过或失败不太感兴趣,而对哪些功能已准备好部署到生产中更感兴趣。

One of the core concepts in BDD is something we call feature readiness. Features, in this sense, are just pieces of functionality that the stakeholders care about. Some teams use user stories in this role, and others prefer to distinguish user stories from higher-level features. The principle is the same in both cases: when the development team reports on progress, stakeholders are less interested in which individual tests pass or fail and are more interested in what functionality is ready to be deployed to production.

16.2.1 功能就绪:哪些功能已准备好交付

16.2.1 Feature readiness: What features are ready to deliver

按照 BDD 术语,当某项功能的所有验收标准都通过时,该功能便可视为准备就绪(或已完成)(见图 16.3)。如果您可以自动执行每一项验收标准,那么自动化测试报告便可以让您简单、简洁地了解正在构建的功能的状态。在此级别,您更感兴趣的是与某项功能相关的所有场景的整体结果,而不是单个场景是否通过。

In BDD terms, a feature can be considered ready (or done) when all of its acceptance criteria pass (see figure 16.3). If you can automate each of these acceptance criteria, then the automated test reports can give you a simple, concise view of the state of the features you’re building. At this level, you’re more interested in the overall result of all the scenarios associated with a feature than with whether individual scenarios pass or fail.

大多数 BDD 工具至少提供某种程度的功能就绪报告,其中场景结果在功能级别汇总。例如,Serenity 直接从底层 Cucumber 功能文件提供功能级报告,以及此摘要报告的简明、可通过电子邮件发送版本,可发送给团队成员和关键利益相关者(见图 16.3)。

Most BDD tools provide at least some level of reporting on feature readiness, where scenario results are aggregated at the feature level. For example, Serenity provides feature-level reports directly from the underlying Cucumber feature files, as well as a concise, emailable version of this summary report that can be sent out to team members and key stakeholders (see figure 16.3).

图 16.3 Serenity BDD 报告提供了按需求组织的测试结果摘要

Figure 16.3 A Serenity BDD report providing a summary of the test results, organized by requirements

标准 Cucumber 只提供开箱即用的基本功能报告,但 Cucumber 背后的公司 SmartBear 确实提供了一项名为 Cucumber Reports(https://cucumber.io/blog/open-source/cucumber-reports/)的免费在线报告服务,您可以使用它在线发布和分享有关任何 Cucumber 测试的报告。

Standard Cucumber provides only basic feature reporting out of the box, but SmartBear, the company behind Cucumber, does propose a free online reporting service called Cucumber Reports (https://cucumber.io/blog/open-source/cucumber-reports/) that you can use to publish and share reports about any Cucumber tests online.

例如,在图 16.4 中,你可以看到使用Cucumber Reports库生成的 Cucumber 项目的功能报告在这些报告中,每个功能的状态都是根据相应场景的总体结果来报告的。

In figure 16.4, for example, you can see a feature report for a Cucumber project, generated using the Cucumber Reports library. In these reports the status of each feature is reported based on the overall result of the corresponding scenarios.

图16.4 Cucumber报告在线发布,方便共享。

Figure 16.4 Cucumber reports are published online, making them easy to share.

像图 16.3 和 16.4 这样的简洁报告是了解正在开发的功能的当前状态的好方法,而不会淹没在每个功能的细节中。设想。

Succinct reports like the ones in figures 16.3 and 16.4 are a good way to get an overview of the current state of the features under development, without drowning in the details of each scenario.

在 Cucumber 项目中生成 Cucumber 报告

Generating Cucumber reports in your Cucumber project

您可以通过多种不同的方式在任何 Cucumber 项目(包括使用 Serenity BDD 运行的项目)中激活此报告:

You can activate this reporting in any Cucumber project (including one running with Serenity BDD) in several different ways:

  • 通过设置CUCUMBER_PUBLISH_ENABLED环境变量true

  • By setting the CUCUMBER_PUBLISH_ENABLED environment variable to true

  • 通过在 src/test/resources 中创建一个名为 cucumber.properties 的文件并设置cucumber.publish.enabled属性true

  • By creating a file called cucumber.properties in src/test/resources and setting the cucumber.publish.enabled property to true

  • @CucumberOptions通过在注释中设置发布属性true在你的测试运行器类中

  • By setting the publish property in the @CucumberOptions annotation to true in your test runner class

通常仅作为持续集成构建作业的一部分来生成和发布这些报告是很有用的,因此使用环境变量通常是最常见的选择。

Often it is useful to only generate and publish these reports as part of a continuous integration build job, so using the environment variable is generally the most common option.

16.2.2 功能覆盖范围:已构建了哪些需求

16.2.2 Feature coverage: What requirements have been built

功能就绪报告可以超越简单地汇总传统的自动化测试结果。理想情况下,功能就绪性还应考虑尚未实现且不存在自动化测试的需求。我们将这种更全面的功能就绪性形式称为功能覆盖率

Feature-readiness reporting can go beyond simply aggregating conventional automated test results. Ideally, feature readiness should also take into account requirements that haven’t been implemented and for which no automated tests exist. We’ll call this more comprehensive form of feature readiness feature coverage.

功能覆盖率会告诉您每个需求已定义和自动化了多少验收标准。它还会告诉您哪些需求没有自动验收标准。

Feature coverage tells you how many acceptance criteria have been defined and automated for each requirement. It also tells you what requirements have no automated acceptance criteria.

例如,假设我们想允许已确认的用户登录他们的飞行常客账户。用户可以使用注册表单注册,也可以使用第三方(如 Google 或 Facebook)通过 SSO 登录。但是,对于当前版本,我们只想通过注册表单实现注册。我们可以编写一个功能文件来表示此要求,如下所示。

For example, suppose we want to allow confirmed users to log in to their Frequent Flyer accounts. Users can either register using a registration form or sign in via SSO using a third party such as Google or Facebook. However, for the current release, we only want to implement registration via the registration form. We could write a feature file to represent this requirement, along the following lines.

清单 16.1 身份验证功能文件

Listing 16.1 The Authentication feature file

业务需求:认证
 
  已注册的飞行常客会员可以使用其 
邮箱和密码
 
  规则:1)常旅客可以通过输入凭证进行身份验证 
在登录页面
    场景:Trevor 成功登录飞行常客计划应用程序     
      鉴于Trevor 已注册为飞行常客计划会员
      他已确认他的电子邮件地址
      然后他应该能够登录飞行常客计划应用程序
 
  规则:2)常旅客可以使用他们的 Google 或 
 Facebook 账户
    示例:Trevor 使用他的 Google 凭证登录                
    示例:Trevor 使用他的 Facebook 凭证登录              
Business Need: Authentication
 
  Registered Frequent Flyer members can access their account using their 
 email and password
 
  Rule: 1) Frequent Flyers can authenticate by entering their credentials 
 on the login page
    Scenario: Trevor successfully logs on to the Frequent Flyer app     
      Given Trevor has registered as a Frequent Flyer member
      And he has confirmed his email address
      Then he should be able to log on to the Frequent Flyer application
 
  Rule: 2) Frequent Flyers can sign in with SSO using their Google or 
 Facebook account
    Example: Trevor logs in using his Google credentials                
    Example: Trevor logs in using his Facebook credentials              

这个场景已经完全定义好了。

This scenario is fully defined.

这些场景被列为验收标准,但尚未实施。

These scenarios are listed as acceptance criteria but haven’t been implemented.

注意到最后两个例子是空的吗?Cucumber 允许我们定义空的场景或示例,例如这些,以表明它们尚未实现。

Notice how the last two examples are empty? Cucumber allows us to define empty scenarios or examples like these ones to indicate that they have not been implemented.

在图 16.5 中,您可以看到实际效果。身份验证功能中所有实施的自动验收标准均通过。但是,功能覆盖率指标仅为 33% 左右,因为三个场景中有两个尚未实施,因此尽管所有测试都通过了,但此功能仅完成了约三分之一。

In figure 16.5, you can see this in action. All of the implemented automated acceptance criteria in the Authentication feature pass. However, the feature coverage metric is only around 33% because two of the three scenarios remain to be implemented, so although all of the tests are green, this feature is only around one-third complete.

图 16.5 未完成的场景在功能覆盖报告中被视为“待处理”。

Figure 16.5 Incomplete scenarios are counted as “pending” in a feature coverage report.

功能覆盖率与代码覆盖率不同。传统的代码覆盖率报告在单元测试和集成测试期间执行了多少行代码。代码覆盖率可以成为一种有用的指标,可以告诉开发人员代码库的哪些部分尚未得到很好的测试。但是,代码覆盖率无法告诉您应用程序是否经过了有效测试,或者应用程序是否按预期执行,只能告诉您在自动化测试期间执行了多少代码。

Feature coverage isn’t the same as code coverage. Traditional code coverage reports how many lines of code were exercised during the unit and integration tests. Code coverage can be a useful metric to tell developers what parts of the code base have not been well tested. However, code coverage can’t tell you whether an application has been tested effectively or whether the application does what it is supposed to do, only how much code was executed during the automated tests.

功能覆盖率背后的想法是提供比仅报告测试结果更公平的项目进度总体情况。功能覆盖率从已定义的需求而不是已执行的测试的角度进行报告。当然,有一个警告:功能覆盖率报告的详尽程度取决于它所了解的总体需求数量。正如您所见,BDD 从业者和敏捷项目通常避免预先定义不必要的更详细的需求。故事和场景通常适用于当前迭代,但仅此而已;除此之外,产品待办事项通常包含更高级别的功能和 Epic。

The idea behind feature coverage is to give a fairer overall picture of project progress than you’d get by just reporting on test results. Feature coverage reports from the point of view of the requirements that have been defined rather than the tests that have been executed. Of course, there’s a caveat: a feature coverage report will only be as thorough as the number of overall requirements it knows about. As you’ve seen, BDD practitioners, and Agile projects in general, avoid defining more detailed requirements up front than necessary. Stories and scenarios will typically be available for the current iteration, but not for much more; beyond that, the product backlog will typically contain higher-level features and Epics.

为了生成这种高级报告,BDD 报告工具需要了解应用程序需求,而不仅仅是从测试报告中获得的信息。测试结果可以告诉您测试了哪些功能,但无法告诉您哪些功能根本没有测试。实现此目的的一种流行方法是将 BDD 报告流程与数字产品积压。

To produce this sort of high-level report, the BDD reporting tool needs knowledge about the application requirements beyond what can be obtained from the test reports. Test results can tell you what features were tested, but they can’t tell you which features have no tests at all. One popular way to achieve this is to integrate the BDD reporting process with a digital product backlog.

16.3 整合数字产品待办事项

16.3 Integrating a digital product backlog

敏捷项目中,任务板经常用于跟踪项目活动。任务板是一块实体板或墙,上面有代表用户故事、任务、错误修复和团队为完成项目必须进行的其他活动的索引卡。您可以在图 16.6 中看到一个简单的示例。

In Agile projects, task boards are frequently used to keep track of project activity. A task board is a physical board or wall containing index cards that represent user stories, tasks, bug fixes, and other activities the team must undertake to complete the project. You can see a simple example in figure 16.6.

图 16.6 任务板是项目中当前正在进行的活动的直观表示。

Figure 16.6 A task board is a visual representation of the activities currently going on in a project.

任务板的具体布局非常灵活,通常每个团队和项目都不同,但一般原则始终相同。在初始规划会议期间,工作被分解为当前迭代期间需要交付的用户故事、任务等。每个任务或用户故事根据其状态(未开始、进行中、完成等)放在板上的一列中。每天,团队成员都会聚集在任务板前讨论他们正在处理的任务的状态,可能会将卡片从一列移动到另一列。这种格式的主要优点是让整个团队一目了然地看到每个人都在做什么,从而更容易协调工作和排除障碍。

The exact layout of a task board is very flexible and is generally different for each team and project, but the general principle is always the same. During an initial planning session, work is broken down into the user stories, tasks, and so on that need to be delivered during the current iteration. Each task or user story goes in a column on the board, based on its status (not started, in progress, done, etc.). Each day, team members get together in front of the task board to discuss the status of the task they’re working on, possibly moving the cards from one column to another. The main advantage of this format is to let the whole team see at a glance what everyone is working on, making it easier to coordinate work and troubleshoot blockages.

实体板是极好的沟通工具,它们为当前正在进行的工作提供了极大的可视性。但它们也有局限性。例如,它们对于不在同一地点的团队来说并不是最佳选择,维护它们可能很耗时,而且如果以电子方式记录任务,许多有用的指标可以更容易地跟踪和可视化。

Physical boards are excellent communication facilitators, and they provide great visibility for the current work in progress. But they do have their limitations. For example, they aren’t optimal for teams that aren’t co-located, they can be time-consuming to maintain, and there are many useful metrics that can be more easily tracked and visualized if the tasks are recorded electronically.

由于这些限制,一些团队更喜欢在某种问题跟踪或敏捷项目管理系统中跟踪工作,即使他们仍然使用物理板进行日常组织和可视性。一些从业者将此称为数字产品待办事项。这是我们今后将使用的术语(因为它比“敏捷项目管理软件”更容易说)。数字产品待办事项可以通过自动计算和报告燃尽图和其他指标来节省您的时间和精力。(燃尽图是 Scrum 项目中常用的图形表示,用于将冲刺中剩余的工作量与冲刺计划的工作量进行比较。)将额外信息附加到卡片上也更容易,而不会使板子变得杂乱。

Because of these constraints, some teams prefer to keep track of work in some sort of issue-tracking or Agile project-management system, even if they still use a physical board for day-to-day organization and visibility. Some practitioners refer to this as a digital product backlog. This is the term we’ll use going forward (because it’s a lot easier to say than “Agile project-management software”). A digital product backlog can save you time and effort by calculating and reporting burndown charts and other metrics automatically. (A burndown chart is a graphical representation commonly used in Scrum projects that compares the amount of work remaining in a sprint to the work planned for the sprint.) It’s also easier to attach extra information to a card without cluttering up the board.

在这种情况下,团队成员(例如 Scrum 团队中的 Scrum 主管)通常会根据围绕物理板的每日会议结果更新数字产品待办事项。在与 BDD 工具集成时,将用户故事存储在电子系统中也具有许多优势。在最简单的形式中,这种集成可能仅包含指向 Agile 项目管理软件中相应卡片的链接。其他工具(例如 Serenity BDD)允许双向集成,以便可以根据执行的测试结果更新 JIRA 用户故事的状态。

In this situation, a team member (e.g., the scrum master in a scrum team) will typically update the digital product backlog items based on the outcomes of the daily meeting around the physical board. Storing user stories in an electronic system also has a number of advantages when it comes to integrating with BDD tools. In the simplest form, this integration may just include links to the corresponding card in the Agile project-management software. Other tools, such as Serenity BDD, allow two-way integration so that the state of a JIRA user story can be updated based on the result of the executed test.

将 Cucumber 场景与数字产品待办事项集成的一种简单方法是使用标签。功能和用户故事被捕获在产品待办事项工具中,它们具有唯一的编号。验收标准以 BDD 功能文件的形式存储。在这些功能文件中,您可以使用特定标签来指示产品待办事项中相应项目的编号。

One simple way to integrate Cucumber scenarios with a digital product backlog is to use tags. Features and user stories are captured in the product backlog tool, where they have a unique number. The acceptance criteria are stored in the form of BDD feature files. Within these feature files, you use a specific tag to indicate the number of the corresponding items in the product backlog.

Serenity BDD 使用@Issue标签将动态文档中的特性、规则和示例链接到问题跟踪系统中的相应问题(见图 16.7)。例如,假设清单 16.1 中的身份验证要求由 JIRA Epic FHFF-3 表示,其中包含两个代表两个规则的用户故事(FHFF-1 和 FHFF-4)。我们可以将这些注释添加到清单 16.1 中的特性文件中,以将特性、规则和示例链接到相应的 JIRA 问题,如下所示:

Serenity BDD uses the @Issue tag to link features, rules, and examples in the living documentation to the corresponding issues in the issue tracking system (see figure 16.7). For example, suppose our authentication requirement from listing 16.1 is represented by a JIRA Epic FHFF-3, which contains two user stories (FHFF-1 and FHFF-4) that represent the two rules. We could add these annotations to the feature file from listing 16.1, to link the features, rules, and examples to the corresponding JIRA issues, as shown here:

@问题:FHFF-                                                               3❶
业务需求:身份验证
 
已注册的飞行常客会员可以使用其 
邮箱和密码
 
  @问题:FHFF-1                                                             
  规则:1)常旅客可以通过输入凭证进行身份验证 
在登录页面
    示例:Trevor 成功登录飞行常客计划应用程序
      鉴于 Trevor 已注册为飞行常客计划会员
      他已确认他的电子邮件地址
      然后他应该能够登录飞行常客计划应用程序
 
  @问题:FHFF-4                                                             
  规则:2)常旅客可以使用他们的 Google 或 
 Facebook 账户
    示例:Trevor 使用其 Google 凭证登录
    示例:Trevor 使用其 Facebook 凭证登录
@Issue:FHFF-3                                                              
Business Need: Authentication
 
Registered Frequent Flyer members can access their account using their 
 email and password
 
  @Issue:FHFF-1                                                            
  Rule: 1) Frequent Flyers can authenticate by entering their credentials 
 on the login page
    Example: Trevor successfully logs on to the Frequent Flyer app
      Given Trevor has registered as a Frequent Flyer member
      And he has confirmed his email address
      Then he should be able to log on to the Frequent Flyer application
 
  @Issue:FHFF-4                                                            
  Rule: 2) Frequent Flyers can sign in with SSO using their Google or 
 Facebook account
    Example: Trevor logs in using his Google credentials
    Example: Trevor logs in using his Facebook credentials

将整个功能链接到 JIRA Epic FHFF-3

Links the feature as a whole to the JIRA Epic FHFF-3

将第一条规则链接到 JIRA 用户故事 FHFF-1

Links the first rule to JIRA User Story FHFF-1

将第二条规则链接到 JIRA 用户故事 FHFF-4

Links the second rule to JIRA User Story FHFF-4

图 16.7 将 JIRA 问题链接到 Serenity BDD 报告中的功能文件

Figure 16.7 Linking JIRA issues to feature files in a Serenity BDD report

注意:您可以通过在 serenity.conf 文件中指定 jira.url 属性来配置 Serenity BDD 和 Serenity JS 以包含指向 JIRA 问题的链接:jira.url = https://myorg.atlassian.net。

NOTE You can configure Serenity BDD and Serenity JS to include links to JIRA issues by specifying the jira.url property in the serenity.conf file: jira.url =https://myorg.atlassian.net.

16.4 利用产品待办事项工具实现更好的协作

16.4 Leveraging product backlog tools for better collaboration

Gherkin 格式作为协作工具的缺点之一是功能文件需要存储在版本控制系统(如 Git)中。虽然使用 Serenity BDD 等报告工具发布实时文档相对容易,但在开发环境中编写和编辑功能文件本身仍然更方便。

One of the disadvantages of the Gherkin format as a collaboration tool is that the feature files need to be stored in a version control system (such as Git). While it is relatively easy to publish living documentation using reporting tools such as Serenity BDD, writing and editing the feature files themselves is still more convenient to do in a development environment.

这可能会给非开发人员带来障碍,他们发现协作编写或更新可执行规范更加困难。一些商业工具(如 Cucumber Studio 和 Behave Pro)旨在让非开发人员更容易协作定义和编写可执行规范。例如,Cucumber Studio 提供了一个在线环境,团队可以在其中协作发现需求和编写 Cucumber 场景(见图 16.8)。

This can create a barrier for nondevelopers, who find it harder to collaborate on writing or updating the executable specifications. Some commercial tools, such as Cucumber Studio and Behave Pro, aim to make it easier for nondevelopers to collaborate on defining and writing executable specifications. Cucumber Studio, for example, provides an online environment where teams can collaborate on requirements discovery and on writing Cucumber scenarios (see figure 16.8).

图 16.8 Cucumber Studio 可以轻松在线运行示例映射会话。

Figure 16.8 Cucumber Studio make it easy to run Example Mapping sessions online.

Behave Pro 可以直接在 JIRA 中用 Gherkin 编写验收标准(见图 16.9),并让它们自动与您的版本控制系统同步。这两种工具还可以与测试执行集成,以便将测试结果反馈到工具中并与可执行规范一起显示。

Behave Pro makes it possible to write acceptance criteria in Gherkin directly in JIRA (see figure 16.9) and have them synchronize automatically with your version control system. Both of these tools can also integrate with test execution so that test results can be fed back into the tool and be displayed alongside the executable specifications.

图 16.9 现在,一些工具允许您直接从问题跟踪软件(例如 JIRA)使用功能文件。

Figure 16.9 Some tools now allow you to work with feature files directly from issue tracking software such as JIRA.

这些工具仍然相对较新且不成熟,并且通常缺乏对最新 Gherkin 功能的支持。但是,它们发展迅速,当您需要确定实时文档时,绝对值得为您自己的项目评估它们战略。

These tools are still relatively new and immature and often lack support for the most recent Gherkin features. However, they are evolving fast, and it will certainly be worth evaluating them for your own projects when you need to determine the living documentation strategy.

16.5 组织动态文档

16.5 Organizing the living documentation

如果为了对整个团队有用,动态文档需要以易于理解和浏览的方式呈现。对于大型项目,单一的功能列表很快就会变得难以处理。

If it’s to be useful to the team as a whole, living documentation needs to be presented in a way that’s easy to understand and navigate. For large projects, a flat list of features can quickly become unwieldy.

幸运的是,有很多方法可以构建动态文档,使其更易于浏览,从而更有价值。在这里,我们将介绍两种最常见的方法:

Fortunately, there are many ways to structure living documentation so that it’s easier to navigate and, as a result, is more valuable. Here, we’ll look at two of the most common approaches:

  • 组织动态文档以反映项目的需求层次

  • Organizing living documentation to reflect the requirements hierarchy of the project

  • 使用标签根据跨职能关注点来组织动态文档

  • Organizing living documentation according to cross-functional concerns by using tags

请注意,这些选择并不唯一。一套好的动态文档应该是灵活的,并且应该能够根据项目和读者的需求以不同的方式轻松显示文档。让我们首先看看如何根据项目需求的结构来组织动态文档。

Note that these choices are not exclusive. A good set of living documentation should be flexible, and it should be possible to easily display documentation in different ways, based on the needs of the project and of the reader. Let’s start with looking at how you can organize your living documentation in terms of the project requirements’ structure.

16.5.1 根据高层需求组织动态文档

16.5.1 Organizing living documentation by high-level requirements

分组按高级需求(例如 Epic 或功能)汇总功能是功能平面列表的一个很好的替代方案。您已经在 Serenity BDD 报告中看到了这种方法的一个示例,其中高级报告按功能或 Epic 总结了功能状态(见图 16.10)。当动态文档报告与电子产品待办事项集成时,这种方法效果很好,因为动态文档的结构将自动反映待办事项工具中使用的结构。

Grouping features by high-level requirements such as Epics or capabilities is a good alternative to a flat list of features. You’ve seen an example of this approach in the Serenity BDD reports, where a high-level report summarizes the feature status by capability or Epic (see figure 16.10). This works well when the living documentation reporting is integrated with an electronic product backlog, because the structure of the living documentation will automatically reflect the structure used in the backlog tool.

图 16.10 组织活文档的一个有用方法是根据高级需求,例如史诗或能力。

Figure 16.10 One useful way to organize living documentation is in terms of high-level requirements, such as Epics or capabilities.

有时,这种层级组织的结构有点过于死板。正如您将在下一节中看到的那样,标签可以提供更灵活的选项。

Sometimes the structure that comes from this sort of hierarchical organization is a little too rigid. As you’ll see in the next section, tags can offer a more flexible option.

16.5.2 使用标签组织动态文档

16.5.2 Organizing living documentation using tags

那里有时以更自由的方式对功能进行分组很有用。例如,您可能希望检查与特定非功能性需求(如安全性或性能)相关的所有功能,或者需要与特定外部系统集成的所有功能。

There are times when it’s useful to group features in a more free-form manner. For example, you might want to examine all the features related to particular nonfunctional requirements, such as security or performance, or all of the features that need to integrate with a particular external system.

标签是一种识别此类非功能性或跨功能性需求的简单方法。例如,以下场景已被标记为与安全相关的非功能性要求:

Tags are an easy way to identify nonfunctional or cross-functional requirements like this. For example, the following scenario has been tagged as a security-related nonfunctional requirement:

@问题:FHFF-4
@security:sso        
规则:2)常旅客可以使用他们的 Google 或 Facebook 帐户通过 SSO 登录
  示例:Trevor 使用其 Google 凭证登录
  示例:Trevor 使用其 Facebook 凭证登录
@Issue:FHFF-4
@security:sso       
Rule: 2) Frequent Flyers can sign in with SSO using their Google or Facebook account
  Example: Trevor logs in using his Google credentials
  Example: Trevor logs in using his Facebook credentials

确定此业务规则及其包含的示例与安全/SSO 非功能性要求相关

Identifies this business rule, and the examples it contains, as relating to the Security/SSO nonfunctional requirement

16.5.3 发布报告的动态文档

16.5.3 Living documentation for release reporting

活的文档是从需求角度了解项目总体状况的好方法。但对于较大的项目,信息量可能有点太大。能够专注于当前迭代中正在完成的工作并屏蔽已经完成的工作或计划在未来发布的工作很有用。

Living documentation is a great way to get a view of the overall project status from the point of view of the requirements. But for larger projects, the quantity of information can be a little overwhelming. It’s useful to be able to focus on the work being done in the current iteration and mask out work that’s already been completed or work that’s scheduled for future releases.

有很多方法可以做到这一点。最简单的方法之一是使用标签将功能或单个场景与特定迭代关联起来。例如,您可以通过为 Cucumber 中的迭代 1 分配适当的标签来为其分配功能、规则或场景:

There are many ways you can do this. One of the simplest is to use tags to associate features or individual scenarios with specific iterations. For example, you could assign a feature, rule, or scenario to iteration 1 in Cucumber by giving it an appropriate tag:

特色:赚取积分
 
  常旅客每次飞行都可以赚取等级积分。
  随着他们赚取更多的积分,他们的地位水平也会提高,他们得到的 
好处。
  
  @release:迭代-1
  规则:会员获得足够的积分即可获得新的会员等级
    场景概述:通过赚取积分来获得地位级别 
级别 <状态级别>
      鉴于Stan 是新的飞行常客会员
      他获得 <最低积分> 和 <最高积分> 之间的积分时
      那么他的状态应该变为<状态等级>
      例如        | 最低积分 | 最高积分 | 状态级别 |
        | 0 | 999 | 标准|
        | 1000 | 1999 | 青铜 |
        | 2000 | 4999 | 银 |
        | 5000 | | 黄金 |
Feature: Earning Points
 
  Frequent Flyers earn status points each time they fly.
  As they earn more points, their status level increases and they get more 
 benefits.
  
  @release:iteration-1
  Rule: Members achieve new status levels when they earn sufficient points
    Scenario Outline: Earning status levels from points earned for status 
 level <Status Level>
      Given Stan is a new Frequent Flyer Member
      When he earns between <Min Points> and <Max Points> points
      Then his status should become <Status Level>
      Examples:
        | Min Points | Max Points | Status Level |
        | 0          | 999        | STANDARD     |
        | 1000       | 1999       | BRONZE       |
        | 2000       | 4999       | SILVER       |
        | 5000       |            | GOLD         |

@release:iteration-1然后,您可以针对包含标签的功能运行单独的一批验收测试,仅关注计划用于此迭代的功能,或者仅查看专用于此标签的报告页面(见图 16.11)。报告的这一部分将仅包含计划用于此版本的场景和功能,从而更清楚地了解应用程序的发布准备情况。

You could then run a separate batch of acceptance tests for the features containing the @release:iteration-1 tag, focusing only on the features scheduled for this iteration, or simply view the report page dedicated to this tag (see figure 16.11). This part of the report will contain only the scenarios and features that have been planned for this release, providing a clearer understanding of how ready the application is for release.

图 16.11 发布报告重点关注已安排用于特定发布或迭代的需求。

Figure 16.11 A release report focuses on requirements that have been scheduled for a particular release or iteration.

您不必坚持使用单一方法来编写动态文档。在实践中,团队通常会针对不同的目的或不同的受众使用多种策略。例如,基于需求的组织更适合记录应用程序整体的功能及其执行方式,而以发布为中心的报告更适合报告进度和准备发布笔记。

You don’t have to stick with a single approach for your living documentation. In practice, teams often use a mix of several strategies for different purposes or different audiences. For example, a requirements-based organization is more effective for documenting what the application does as a whole and how it does it, whereas release-focused reporting is more relevant when reporting on progress and preparing release notes.

16.5.4 低级动态文档

16.5.4 Low-level living documentation

活的文档并不止于高层次的需求。测试驱动开发 (TDD) 涉及以低级可执行规范的形式编写单元测试和集成测试,这构成了 BDD/TDD 项目中技术文档的主要部分。

Living documentation doesn’t stop with the high-level requirements. Test-Driven Development (TDD) involves writing unit and integration tests in the form of low-level executable specifications, which form a major part of the technical documentation in a BDD/TDD project.

对于技术动态文档,复杂的报告功能不如代码的可读性和清晰度重要。技术文档的主要受众是以后需要维护代码的开发人员。开发人员习惯于阅读代码,因此以组织良好、可读、带注释的代码示例形式呈现的技术文档通常足以帮助他们理解代码库的细节。许多团队使用轻量级、高级架构文档(例如存储在项目上)来补充低级动态文档维基百科。

For technical living documentation, sophisticated reporting capabilities are less important than code readability and clarity. The primary audience of technical documentation is the developer who will need to maintain the code later on. Developers are used to reading code, so technical documentation in the form of well-organized, readable, annotated code samples is generally quite sufficient to help them understand the finer points of the code base. Many teams complement the low-level living documentation with light, higher-level architectural documentation, stored, for example, on a project wiki.

16.5.5 单元测试作为动态文档

16.5.5 Unit tests as living documentation

许多现代单元测试工具使单元测试可以很容易地表达为动态文档。我们在第 3 章中看到了如何使用 Java 中的 JUnit 5 编写非常易读的单元和集成测试(例如,参见清单 3.3)。还有许多 JavaScript 测试工具可以非常轻松地编写清晰、自文档化的单元测试:

Many modern unit testing tools make it easy express unit tests as living documentation. We saw in chapter 3 how we can use JUnit 5 in Java to write very readable unit and integration tests (see, for example, listing 3.3). There are also plenty of JavaScript testing tools that make writing clear, self-documenting unit tests very easy:

描述('FrequentFlyerController',()=> {
    ...
    describe('创建新账户时', () => {
 
        it('应该为每个帐户生成一个新号码', async () => {
            const result = 等待控制器.创建(newFrequentFlyer);
            期望(结果.frequentFlyerNumber).toBeDefined()
        })
 
        it('新的飞行常客帐户应处于待处理状态', async () => {
            const result = 等待控制器.创建(newFrequentFlyer);
            const 频繁飞行者号码 = 结果.频繁飞行者号码;
 
            const oftenFlyerAccount = 控制器.findByFrequentFlyerNumber(frequentFlyerNumber)
            预期(frequentFlyerAccount.isActivated).toBeFalsy()
        });
 
        it('如果电子邮件无效,则应返回错误', async () => {
            const 结果 = 控制器.创建(frequentFlyerWithAnInvalidEmail);
            等待期望(结果)。rejects.toThrow(“无效的电子邮件地址”);
        })
    });
    ...
});
describe('FrequentFlyerController', () => {
    ...
    describe('When creating a new account', () => {
 
        it('should generate a new number for each account', async () => {
            const result = await controller.create(newFrequentFlyer);
            expect(result.frequentFlyerNumber).toBeDefined()
        })
 
        it('new Frequent Flyer accounts should be Pending', async () => {
            const result = await controller.create(newFrequentFlyer);
            const frequentFlyerNumber = result.frequentFlyerNumber;
 
            const frequentFlyerAccount = controller.findByFrequentFlyerNumber(frequentFlyerNumber)
            expect(frequentFlyerAccount.isActivated).toBeFalsy()
        });
 
        it('should return an error if the email is invalid ', async () => {
            const result = controller.create(frequentFlyerWithAnInvalidEmail);
            await expect(result).rejects.toThrow("Invalid email address");
        })
    });
    ...
});

但是编写单元测试以形成良好的技术文档更多地依赖于态度而不是使用特定工具。例如,以下 NSpec 规范也很好地解释了它所描述的功能以及应如何使用 API:

But writing unit tests that make good technical documentation relies more on an attitude than on using a particular tool. The following NSpec specification, for example, also does a great job of explaining what feature it’s describing and illustrating how an API should be used:

公共类 WhenUpdatingStatusPoints : nspec
{
    飞行常客计划会员;
    无效 before_each()
    {
        会员=新的FrequentFlyer();
    }
    无效收入状态点数()
    {
       context["累积飞行常客积分时"] = () =>
       {
          it["每次飞行都应获得积分"] = () =>
          {
             会员.获得状态积分(100);
             会员.获得状态积分(50);
            成员.getStatusPoints()。应该是(150);
          };
          it["当获得足够的积分时应该升级状态"] = () =>
          {
              会员.获得状态积分(300);
              成员.getStatus()。应该是(状态.银);
          };
       };
    };
};
public class WhenUpdatingStatusPoints : nspec
{
    FrequentFlyer member;
    void before_each()
    {
        member = new FrequentFlyer();
    }
    void earning_status_points()
    {
       context["When cumulating Frequent Flyer points"] = () =>
       {
          it["should earn points for each flight"] = () =>
          {
             member.earnStatusPoints(100);
             member.earnStatusPoints(50);
            member.getStatusPoints().should_be(150);
          };
          it["should upgrade status when enough points are earned"] = () =>
          {
              member.earnStatusPoints(300);
              member.getStatus().should_be(Status.Silver);
          };
       };
    };
};

当代码库变得庞大时,很难找到说明特定功能的单元测试。许多团队喜欢将他们的单元测试组织在与应用程序结构相似的包结构中。这种方法的优点是可以更轻松地找到特定测试的测试类,尽管这种优势在当今的开发环境中不那么引人注目,因为在当今的开发环境中,很容易找到代码库中使用过特定类的所有位置。

When a code base grows large, it can be hard to find the unit test that illustrates a particular feature. Many teams prefer to organize their unit tests in a package structure that mirrors the application structure. This approach has the advantage of making it easier to find the test class for a particular test, although this advantage is much less compelling with today’s development environments, where it’s very easy to find all the places in the code base where a particular class has been used.

这种方法可以追溯到单元测试的早期。最初的动机之一是给定包中的 Java 类可以访问该包中其他类的受保护字段,因此如果单元测试类与被测试的类位于同一包中,那么您可以在单元测试中访问受保护字段。

This approach dates from the early days of unit testing. One of the original motivations was that Java classes in a given package could access protected fields of other classes in that package, so if the unit test class was in the same package as the class being tested, you could access protected fields as part of the unit tests.

从 BDD 的角度来看,这种方法的论据不太有说服力。BDD 样式的单元测试应该提供一个实际示例,说明如何使用类以及该类的预期行为。如果您需要访问受保护的变量,则可能会将测试代码与实现绑定得太紧,这有使单元测试更脆弱的风险。

From a BDD perspective, the argument for this approach is less compelling. A BDD-style unit test should provide a worked example of how to use a class and how the class is expected to behave. If you need to access protected variables, you may be binding your test code too tightly to the implementation, which runs the risk of making the unit tests more brittle.

这种方法还假设被测试的类与这些类正在实现的功能(或行为)之间存在非常紧密的耦合。例如,如果您重构一个类、更改其名称或将其拆分为几个较小的类,则这些类实现的要求不应改变。

This approach also assumes a very tight coupling between the classes under test and the features (or behavior) those classes are implementing. If you refactor a class, change its name, or break it into several smaller classes, for example, the requirement that these classes implement shouldn’t change.

为每个生产类设置一个测试类也会妨碍重构。现代 IDE 会一眼就告诉你某个方法或类是否在任何地方都没有使用。除非它是外部客户端 API 的一部分,否则通常可以删除从未在任何地方使用的方法,这样可以减少以后需要维护的代码。

Having a test class for each production class can also hamper refactoring. A modern IDE will tell you at a glance if a method or a class isn’t being used anywhere. Unless it’s part of an API for an external client, a method that’s never used anywhere can generally be deleted, which results in less code to maintain going forward.

出于所有这些原因,许多组织在测试类和生产类之间采用了较松散的关联。例如,一些团队发现按功能片段组织的测试包或目录通常更容易导航,尤其是当你在完成一段时间后返回代码库时长的时期。

For all these reasons, many organizations apply a looser association between test classes and production classes. For example, some teams find that test packages or directories organized in terms of functional slices are often easier to navigate, especially when you come back to a code base after a long period.

16.6 遗留应用程序的动态文档

16.6 Living documentation for legacy applications

许多组织拥有大量仍在使用的遗留应用程序,这些应用程序仍需要定期维护、更新和发布。其中许多应用程序的单元测试或集成测试很少,测试覆盖率很低,而且它们通常缺乏技术或功能文档。

Many organizations have large legacy applications that are still very much in use and still need regular maintenance, updates, and releases. Many of these applications have few unit or integration tests and low test coverage, and they often lack technical or functional documentation.

此类应用程序缺乏自动化测试,因此很难快速提供新功能或修复错误,风险也更大。发布会因测试周期长和回归问题而延迟。但对于许多组织来说,重写整个应用程序并不是一个可行的方案。

The lack of automated tests for this sort of application makes it harder and riskier to deliver new features or bug fixes quickly. Releases are delayed by long testing cycles and regression issues. But for many organizations, rewriting the entire application isn’t a viable proposition.

BDD 提供了一些可能的方法,可以缓解这些症状并帮助团队更安全、更高效地向其遗留应用程序交付更改。许多团队采用的一种流行策略是使用高级验收测试(通常是 Web 应用程序的 Web 测试)改造遗留应用程序。这些验收测试既描述又记录现有系统,并有助于降低引入新功能时出现回归的风险。这些测试的编写风格与您在本书其他地方看到的自动验收标准相同,并且通常使用相同的 BDD 重点工具,例如 Cucumber 和 SpecFlow。这些工具的 BDD 报告功能是记录和传达人们认为应用程序应该如何表现的好方法。

BDD offers some possible approaches that can relieve these symptoms and help teams to deliver changes to their legacy applications more safely and efficiently. One popular strategy that many teams adopt is to retrofit the legacy application with high-level acceptance tests, typically web tests for a web application. These acceptance tests both describe and document the existing system and help reduce the risk of regressions when new features are introduced. The tests are written in the same style as the automated acceptance criteria you’ve seen elsewhere in this book, and often with the same BDD-focused tools such as Cucumber and SpecFlow. The BDD reporting capabilities of these tools are a great way to document and communicate how people believe the application should behave.

传统上,很难有效地改进单元测试,因为在代码编写后太久编写的单元测试往往非常肤浅。BDD 单元测试指定了这些应用程序中类和组件的行为,并且通常是记录此行为的唯一地方。当这些规范不存在时,事后发明它们可能很困难,因为它需要深入了解每个类或组件的预期行为。

Retrofitting unit testing is traditionally very difficult to do effectively because unit tests written too long after the code was written tend to be quite superficial. BDD unit tests specify the behavior of classes and components within these applications and are often the only place that this behavior is documented. When these specifications don’t exist, it can be difficult to invent them after the fact, as it involves a deep understanding of how each class or component is expected to behave.

尽管存在这些困难,BDD 单元测试仍可以提供出色的技术文档,即使对于遗留应用程序也是如此。一些拥有任务关键型遗留应用程序的团队仍然需要频繁进行重大更改,他们首先使用 BDD 测试来记录其应用程序中最关键或高风险的部分,然后再扩展到不太重要的功能。单元测试的编写旨在记录当前应用程序行为并提供如何使用每个类的代码示例。这是一种为应用程序中更关键的部分提供技术文档并建立高质量测试的好方法覆盖范围。

Despite these difficulties, BDD unit tests can provide excellent technical documentation, even for legacy applications. Some teams with mission-critical legacy applications that still require frequent and significant changes use BDD tests to document the most critical or high-risk parts of their application first, before expanding into less critical functionality. Unit tests are written in the spirit of documenting the current application behavior and giving code samples for how to use each class. This is a great way to provide technical documentation for the more critical parts of the application and to build up high-quality test coverage.

概括

Summary

  • 动态文档是一种自动报告应用程序想要做什么以及如何做的方式。

  • Living documentation is a way of automatically reporting both what an application intends to do and how it does it.

  • 除了单独的测试结果之外,动态文档还提供有关功能和要求的信息。

  • Living documentation provides information about features and requirements in addition to individual test results.

  • 如果您将需求存储在数字产品待办事项工具中,那么将自动验收标准中的 BDD 报告与产品待办事项工具中的需求相结合会很有用。

  • If you store your requirements in a digital product backlog tool, it’s useful to integrate BDD reporting from the automated acceptance criteria with the requirements from the product backlog tool.

  • 活动文档可以以多种方式组织,具体取决于需要检索的信息。

  • Living documentation can be organized in many ways, depending on what information needs to be retrieved.

  • 活文档还可用于记录系统的较低级别的技术组件。

  • Living documentation can also be used to document the lower-level technical components of your system.

  • 动态文档也适用于遗留系统,它可以成为记录和测试现有系统的一种非常有效的方式。应用。

  • Living documentation is also applicable for legacy systems, where it can be a very effective way of both documenting and testing an existing application.

指数

index

符号

Symbols

@After 注释 362363

@After annotation 362363

@After 钩子 236237

@After hook 236237

[@属性=值] 278

[@attribute=value] 278

@backend 标签 236

@backend tag 236

@BeforeAll 钩子 237

@BeforeAll hook 237

@Before 钩子 236 , 241 , 269

@Before hook 236, 241, 269

@CucumberOptions 注释 217 , 238 , 433

@CucumberOptions annotation 217, 238, 433

@DataTypeType 方法 340

@DataTypeType method 340

@DynamicPropertySource 注释 241

@DynamicPropertySource annotation 241

@FindBy 注释 304306

@FindBy annotation 304306

@Given 方法,自动化 63 中的先决条件

@Given method, automating preconditions in 63

@Issue 标签 436

@Issue tag 436

@JsonIgnoreProperties 属性 361

@JsonIgnoreProperties attribute 361

@release:iteration-1 标签 442

@release:iteration-1 tag 442

@serenity-js/assertions 模块 407

@serenity-js/assertions module 407

@serenity-js/核心 384

@serenity-js/core 384

@serenity-js/playwright 模块 423

@serenity-js/playwright module 423

@serenity-js/rest 模块 397 , 401

@serenity-js/rest module 397, 401

@serenity-js/webdriverio 模块 423

@serenity-js/webdriverio module 423

@serenity-js/web 模块 397 , 418 , 423

@serenity-js/web module 397, 418, 423

@smoketest 标签 236

@smoketest tag 236

@Then 注释 54

@Then annotation 54

@web 标签 236

@web tag 236

@When 注释 54

@When annotation 54

@When 方法,在 56 – 57中发现 Service 类 API

@When method, discovering Service class API in 5657

//节点/节点 XPath 表达式 278

//node/node XPath expression 278

//节点 XPath 表达式 278

//node XPath expression 278

/api/frequent-flyer/{id} 端点 361362

/api/frequent-flyer/{id} endpoint 361362

#id 选择器 275

#id selector 275

[.=值] 278

[.=value] 278

一个

A

AAFTT(敏捷联盟功能测试工具研讨会)319

AAFTT (Agile Alliance Functional Testing Tools workshop) 319

能力 328329

abilities 328329

抽象层 248255

abstraction layers 248255

业务流程251,254

business flow layer 251, 254

大局观 252254

big-picture scenarios 252254

业务目标和用户旅程 251252

business goals and user journeys 251252

业务规则层 249251

business rules layer 249251

业务任务 254

business tasks 254

技术层 254255

technical layer 254255

验收测试驱动开发 (ATDD) 18

Acceptance Test–Driven Development (ATDD) 18

验收测试

acceptance tests

微服务自动化 350363

automating for microservices 350363

删除查询 362363

DELETE queries 362363

GET 查询 358359

GET queries 358359

部分 JSON 响应 360362

partial JSON responses 360362

POST 查询 353356

POST queries 353356

准备测试数据 352353

preparing test data 352353

使用 JSONPath 查询 JSON 响应 356358

querying JSON responses with JSONPath 356358

从验收测试到单元测试 57

going from acceptance tests to unit tests 57

AcceptanceTestSuite 测试运行器类 215

AcceptanceTestSuite test runner class 215

账户类别16、27

Account class 16, 27

账户组成部分

accounts component 184

AccountStatus 对象 362

AccountStatus object 362

accountType 变量 26

accountType variable 26

收购

acquisition 99

动作类 316 , 325

Action classes 316, 325

激活100

activation 100

主动语态 400

active voice 400

{actor} 自定义参数类型 390

{actor} custom parameter type 390

{actor} 令牌 389390

{actor} token 389390

actor.attemptsTo(...activities:Activity[ ] API 323、389、391

actor.attemptsTo(...activities: Activity[]) API 323, 389, 391

actorCalled(name) Serenity/JS API 390

actorCalled(name) Serenity/JS API 390

以行动者为中心的方法 318

actor-centric approach 318

Actor.named() 方法 338

Actor.named() method 338

演员

actors

能力 328329

abilities 328329

定义 320321

defined 320321

定义 339 的自定义参数类型

defining custom parameter type for 339

定义影响映射 9496

defining for impact mapping 9496

设计可扩展的测试自动化系统

designing scalable test automation systems

使用演员来描述角色 384386

using actors to describe personas 384386

使用参与者链接各层 383384

using actors to link layers 383384

相互作用 322328

interactions 322328

作为对象 325

as objects 325

与 REST API 交互 328

interacting with REST APIs 328

执行多项 323324

performing multiple 323324

执行等待以及操作 326327

performing waits as well as actions 326327

旅程地图 366368

Journey Mapping 366368

有意义的 193196

meaningful 193196

场景 195 – 196中的角色

personas in scenarios 195196

用户故事中的人物角色 194195

personas in user stories 194195

问题 330334

questions 330334

特定领域问题类别 333

domain-specific Question classes 333

查询系统状态 331332

querying state of system 331332

用来做出断言 333334

using to make assertions 333334

剧本模式和黄瓜 338

Screenplay Pattern and Cucumber 338

任务 321322

tasks 321322

适配器模式 329

Adaptor Pattern 329

Adzic,Gojko 19

Adzic, Gojko 19

AfterAll 钩子 235 , 237

AfterAll hook 235, 237

后钩235

After hook 235

敏捷 33

Agile 33

敏捷联盟功能测试工具研讨会 (AAFTT) 319

Agile Alliance Functional Testing Tools workshop (AAFTT) 319

AJAX 应用程序,测试 285288

AJAX applications, testing 285288

以及关键字 172173

And keyword 172173

Angular Material 库 283

Angular Material library 283

answerBy() 方法 332

answeredBy() method 332

API(应用程序编程接口)

APIs (Application Programming Interfaces)

API 及其测试方法 343344

APIs and how to test them 343344

在 @When 方法中发现 Service 类 API 5657

discovering Service class API in @When method 5657

与 REST API 交互 328

interactions interacting with REST APIs 328

测试 API 或使用 API 进行测试 364

testing APIs or testing with APIs 364

as() 方法 311

as() method 311

asInteger() 方法 332

asInteger() method 332

断言

assertions

页面对象 303304

page objects 303304

Cucumber 340 – 341中的剧本断言

Screenplay assertions in Cucumber 340341

使用问题来制作 333334

using questions to make 333334

AssertJ 库 277

AssertJ library 277

假设 7981

assumptions 7981

异步页面 285288

asynchronous pages 285288

async 关键字 392

async keyword 392

ATDD(验收测试驱动开发)18

ATDD (Acceptance Test – Driven Development) 18

attemptsTo() 方法 323325

attemptsTo() method 323325

.[attribute*=value]. 选择器 276

.[attribute*=value]. selector 276

.[attributeˆ=value]. 选择器 276

.[attributeˆ=value]. selector 276

.[attribute$=value]. 选择器 276

.[attribute$=value]. selector 276

[属性=值] 选择器 275

[attribute=value] selector 275

属性 剧本 问题类别 332

Attribute Screenplay question classes 332

身份验证组件 184

authentication component 184

自动验收测试 207256

automated acceptance tests 207256

抽象层 248255

abstraction layers 248255

业务流层 251254

business flow layer 251254

业务规则层 251

business rules layer 251

业务任务 254

business tasks 254

技术层 254255

technical layer 254255

背景 232238

backgrounds 232238

适用于微服务 350363

for microservices 350363

删除查询 362363

DELETE queries 362363

GET 查询 358359

GET queries 358359

部分 JSON 响应 360362

partial JSON responses 360362

POST 查询 353356

POST queries 353356

准备测试数据 352353

preparing test data 352353

使用 JSONPath 查询 JSON 响应 356358

querying JSON responses with JSONPath 356358

胶水代码 219232

glue code 219232

黄瓜表达 222225

Cucumber expressions 222225

数据表 228232

data tables 228232

使用步骤定义参数注入数据 220221

injecting data with step definition parameters 220221

列表 228

lists 228

正则表达式 225227

regular expressions 225227

钩子 232 , 234238

hooks 232, 234238

Cucumber 事件监听器 238

Cucumber EventListeners 238

获取有关场景 235 的更多信息

getting more information about scenario 235

干预特征 237 – 238的开始和结束

intervening at start and end of feature 237238

干预前后情景 235

intervening before and after scenario 235

干预前后的具体情况 236

intervening before and after specific scenarios 236

准备测试环境 238239

preparing test environments 238239

自动化场景概述 210212

overview of automating scenarios 210212

人物 248

personas 248

以 HOCON 格式存储个人数据 247248

storing persona data in HOCON format 247248

与 246 – 247合作

working with 246247

便携式测试自动化 393426

portable test automation 393426

设计领域层 394412

designing Domain layer 394412

设计便携式集成层 412425

designing portable Integration layer 412425

运行场景 216219

running scenarios 216219

JavaScript 和 TypeScript 218219

in JavaScript and TypeScript 218219

Java 中的测试运行器类 217

test runner classes in Java 217

可扩展测试自动化系统 381386

scalable test automation systems 381386

分层架构 382383

layered architecture 382383

使用演员来描述角色 384386

using actors to describe personas 384386

使用参与者链接各层 383384

using actors to link layers 383384

设置项目 212215

setting up projects 212215

Java 213215

in Java 213215

在 TypeScript 中 213 , 215

in TypeScript 213, 215

步骤定义 210212

step definitions 210212

虚拟测试环境 239242

virtual test environments 239242

编写工业强度的测试 244245

writing industrial-strength tests 244245

自动化 UI 测试 257 , 259314

automated UI testing 257, 259314

动作类 310311

action classes 310311

258 – 259的优势

advantages of 258259

异步页面 285288

asynchronous pages 285288

业务逻辑

business logic

记录和验证屏幕特定 262263

documenting and verifying screen-specific 262263

图示 261262

illustrating 261262

DSL 层和构建器 312314

DSL layers and builders 312314

说明用户旅程 260261

illustrating user journeys 260261

与网页交互 270271

interacting with web pages 270271

非结构化测试脚本的局限性 291292

limitations of unstructured test scripts 291292

现代 UI 库组件 283285

modern UI library components 283285

页面对象 293309

page objects 293309

@FindBy 注释 304306

@FindBy annotation 304306

断言 303304

assertions 303304

查找集合 306308

finding collections 306308

隐藏等待条件 302303

hiding wait conditions 302303

定位元件 293296

locating elements 293296

执行业务任务或模拟用户行为 298300

peforming business tasks or simulating user behavior 298300

以商业术语呈现状态 301302

presenting state in business terms 301302

表示对象 296

representing objects 296

宁静 BDD 308309

Serenity BDD 308309

第 296 – 298页的状态

state of page 296298

WebDriver 页面工厂 304306

WebDriver page factories 304306

查询类 311312

query classes 311312

作为 UI 测试实现的场景 260

scenarios to implement as UI tests 260

Selenium WebDriver

Selenium WebDriver

自动化验收标准 263265

automating acceptance criteria 263265

Java 入门 265266

getting started with in Java 265266

与 Cucumber 集成 269

integrating with Cucumber 269

设置 267268

setting up 267268

在步骤定义类之间共享实例 269

sharing instances between step definition classes 269

将位置逻辑与测试逻辑分离 292293

separating location logic from test logic 292293

显示信息如何呈现 263

showing how information is rendered 263

易于测试的 Web 应用程序 288289

test-friendly web applications 288289

测试 AJAX 应用程序 285288

testing AJAX applications 285288

网页元素

web elements

与 280283交互

interacting with 280283

定位 272280

locating 272280

自动化网络测试 258

automated web tests 258

自动化阶段 4767

automate phase 4767

自动化可执行规范 5455

automating executable specifications 5455

胶水代码 5567

glue code 5567

在 @Given 方法中自动化先决条件 63

automating preconditions in @Given method 63

在 @Then 方法中定义期望 5556

defining expectations in @Then method 5556

在 @When 方法中发现 Service 类 API 5657

discovering Service class API in @When method 5657

从验收测试到单元测试 57

going from acceptance tests to unit tests 57

实施服务 6465

implementing service 6465

实现 TimeTable 服务 65

implementing TimeTable service 65

重构完成代码 6667

refactoring completed code 6667

编写简单的 TDD 测试用例 5861

writing simple TDD test case 5861

在 Cucumber 中记录可执行规范 5153

recording executable specifications in Cucumber 5153

使用 Maven 和 Cucumber 设置项目 4751

setting up project with Maven and Cucumber 4751

await 关键字 392

await keyword 392

B

背景关键字 185186

Background keyword 185186

背景

backgrounds

背景步骤 232234

background steps 232234

232 – 238概述

overview of 232238

平衡变量 26

balance variable 26

BAU(正常经营) 70 , 245

BAU (Business as Usual) 70, 245

BDD(行为驱动开发)334

BDD (behavior-driven development) 334

31 – 32的好处

benefits of 3132

更轻松、更安全的变更 32

easier and safer changes 32

更快的发布 32

faster releases 32

降低成本 32

reduced costs 32

减少浪费 31

reduced waste 31

确定是否适合您的项目 1213

determining if right for your projects 1213

32 – 33的劣势和潜在挑战

disadvantages and potential challenges of 3233

敏捷或迭代环境 33

Agile or iterative context 33

在孤立的开发方法中效果不佳 33

doesn't work well in siloed development approach 33

高度的商业参与和协作 32

high business engagement and collaboration 32

编写不当的测试可能会导致更高的成本 33

poorly written tests can lead to higher costs 33

用于教学 TDD 1517

for teaching TDD 1517

15 – 17的起源

origin of 1517

概述 57

overview of 57

原则和做法 1931

principles and practices 1931

协作方法指定特征 2021

collaborative approach to specifying features 2021

拥抱不确定性 21

embracing uncertainty 21

关注商业价值功能 20

focus on business-value features 20

小黄瓜底漆 2224

Gherkin primer 2224

用具体的例子来说明特点 2122

illustrating features with concrete examples 2122

生活记录 2931

living documentation 2931

单元测试 2628

unit tests 2628

编写可执行规范 2426

writing executable specifications 2426

需要解决的问题 812

problems needing solution 812

构建正确的软件 910

building right software 910

构建软件权利 89

building software right 89

知识限制 1112

knowledge constraint 1112

需求分析 1718

requirements analysis 1718

BDD(行为驱动开发)流程 3572

BDD (behavior-driven development) flow 3572

自动化阶段 4767

automate phase 4767

自动化可执行规范 5455

automating executable specifications 5455

胶水代码 5567

glue code 5567

录制可执行规范 5153

recording executable specifications 5153

设置项目 4751

setting up project 4751

演示阶段

demonstrate phase 68

制定阶段 4547

formulate phase 4547

说明阶段 4245

illustrate phase 4245

发现特征 4243

discovering features 4243

将功能分解为用户故事 4445

slicing features into user stories 4445

36 – 37概述

overview of 3637

降低维护成本 7071

reducing maintenance costs 7071

推测阶段 3742 , 75

speculate phase 3742, 75

描述特征 4142

describing features 4142

发现功能和特点 3940

discovering capabilities and features 3940

目标

goals 85

假设和假定 7981

hypotheses and assumptions 7981

确定业务目标 3839

identifying business objectives 3839

影响图谱 9899

impact mapping 9899

海盗画布 101110

pirate canvases 101110

战略规划 7779

strategic planning 7779

愿景 8185

vision 8185

BeforeAll 钩子 235 , 237 , 384

BeforeAll hook 235, 237, 384

钩 235 前

Before hook 235

{bigdecimal} 参数类型 221

{bigdecimal} parameter type 221

{biginteger} 参数类型 221

{biginteger} parameter type 221

图书链接 280

Book links 280

浏览网页能力 329

BrowseTheWeb ability 329

BrowseTheWeb.as() 静态方法 329

BrowseTheWeb.as() static method 329

浏览网页 424 班

BrowseTheWeb class 424

BrowseTheWebWithPlaywright 类 385 , 424

BrowseTheWebWithPlaywright class 385, 424

BrowseTheWebWithWebdriverIO 类 424

BrowseTheWebWithWebdriverIO class 424

巴克尼,皮特 159

Buckney, Pete 159

燃尽图 436

burndown chart 436

正常经营 (BAU) 70 , 245

Business as Usual (BAU) 70, 245

业务流层 251254

business flow layer 251254

大局观 252254

big-picture scenarios 252254

业务目标和用户旅程 251252

business goals and user journeys 251252

业务逻辑

business logic

记录和验证屏幕特定 262263

documenting and verifying screen-specific 262263

图示 261262

illustrating 261262

业务规则层 251

business rules layer 251

业务任务 254

business tasks 254

但关键词 172173

But keyword 172173

但同义词

but synonym 172

By.className()方法 275

By.className() method 275

By.css、By.xpath元素选择器409

By.css, By.xpath element selector 409

by.css实现414

By.css implementation 414

By.cssSelector() 方法 275

By.cssSelector() method 275

By.deepCss 实现 414

By.deepCss implementation 414

By.id 实施 414

By.id implementation 414

通过选择409、414 – 415

By selector 409, 414415

{byte} 参数类型 221

{byte} parameter type 221

By.xpath() 方法 278

By.xpath() method 278

by.xpath实现414

By.xpath implementation 414

C

CallAnApi 类 386

CallAnApi class 386

CanScheduleService 接口 6364

CanScheduleService interface 6364

能力

capabilities

交付 117120

delivering 117120

发现 3940

discovering 3940

按 182 组织功能文件

organizing feature files by 182

愿景、目标和 8183之间的关系

relationship between vision, goals and 8183

投射接口385、390、424

Cast interface 385, 390, 424

演员 338

casts 338

复选框 282283

check boxes 282283

ChromeDriver 类构造函数 268

ChromeDriver class constructor 268

ChromeOptions 类 268

ChromeOptions class 268

类属性 279

class attribute 279

.class 选择器 275

.class selector 275

clear() 方法 282

clear() method 282

清除交互类 324

Clear interaction class 324

click() 方法 271272 , 280 , 325

click() method 271272, 280, 325

点击交互类 255 , 323325

Click interaction class 255, 323325

点击 API 409

Click.on API 409

合作

collaboration

业务参与度高,32

high business engagement and 32

利用产品待办事项工具实现 438439

leveraging product backlog tools for 438439

指定功能 2021

specifying features 2021

评论 173174

comments 173174

配套页面对象 397

Companion Page Objects 417

概念化 139

conceptualize 139

contains() 函数 279

contains() function 279

[contains(@attribute,value)] XPath 表达式 278

[contains(@attribute,value)] XPath expression 278

containsText 等待条件 327

containsText wait condition 327

成本

costs

编写不佳的测试导致更高的 33

poorly written tests leading to higher 33

减少 32 , 7071

reducing 32, 7071

跨越鸿沟(摩尔) 84

Crossing the Chasm (Moore) 84

CSS,使用 275 – 277识别网页元素

CSS, identifying web elements using 275277

CssClasses.of(pageElement) API 416

CssClasses.of(pageElement) API 416

黄瓜

Cucumber

事件监听器 238

EventListeners 238

表达式

expressions

自定义参数类型和 223225

custom parameter types and 223225

更加灵活 222223

making more flexible 222223

将 Selenium WebDriver 与 269 集成

integrating Selenium WebDriver with 269

将 TestContainers 与 241242集成

integrating TestContainers with 241242

记录可执行规范 5153

recording executable specifications in 5153

运行场景 216219

running scenarios 216219

JavaScript 和 TypeScript 218219

in JavaScript and TypeScript 218219

Java 中的测试运行器类 217

test runner classes in Java 217

剧本模式和 337341

Screenplay Pattern and 337341

演员和演员阵容 338

actors and casts 338

为参与者定义自定义参数类型 339

defining custom parameter type for actors 339

在枚举值中定义角色 339340

defining persona in enum values 339340

Cucumber 340 – 341中的剧本断言

Screenplay assertions in Cucumber 340341

剧本阶段 338339

Screenplay stage 338339

设置项目 212215

setting up projects 212215

自动化阶段 4751

automate phase 4751

Java 213215

in Java 213215

在 TypeScript 中 213 , 215

in TypeScript 213, 215

黄瓜表情220

Cucumber Expression 220

CUCUMBER_PUBLISH_ENABLED 环境变量 433

CUCUMBER_PUBLISH_ENABLED environment variable 433

cucumber.publish.enabled 属性 433

cucumber.publish.enabled property 433

Cucumber 报告库 432

Cucumber Reports library 432

面向客户的可执行规范 27

customer-facing executable specifications 27

D

数据属性 274

data attribute 274

数据表类 229

DataTable class 229

数据表

data tables

复合体 230232

complex 230232

与 229 – 230合作

working with 229230

DataTableType 注释 230

DataTableType annotation 230

data-testid 属性 273

data-testid attribute 273

DDD(领域驱动设计)14

DDD (Domain-Driven Design) 14

deleteFrequentFlyer() 方法 362

deleteFrequentFlyer() method 362

DELETE 查询,自动执行微服务验收测试 362363

DELETE queries, automating acceptance tests for microservices 362363

故意发现 132133

deliberate discovery 132133

可交付成果,定义影响映射 9798

deliverables, defining for impact mapping 9798

演示阶段 36

Demonstrate phase 36

丹宁,斯蒂芬 20

Denning, Stephen 20

DepartingTrainsStepDefinitions 类 54

DepartingTrainsStepDefinitions class 54

离境 () 方法 61

departures() method 61

deposit() 方法 16

deposit() method 16

残障人士班332

Disabled class 332

Docker 容器,使用 TestContainers 进行管理 240242

Docker containers, managing with TestContainers 240242

领域驱动设计 (DDD) 14

Domain-Driven Design (DDD) 14

领域层 382 ,​​ 394412

Domain layer 382, 394412

将交互组合成任务 397398

composing interactions into tasks 397398

实施业务领域任务 396397

implementing business domain tasks 396397

执行验证任务 407412

implementing verification tasks 407412

利用混合测试实现非 UI 交互 399401

leveraging non-UI interactions with blended testing 399401

建模业务领域任务 395396

modeling business domain tasks 395396

由外而内的方法实现任务替代 398399

outside-in approach to enable task substitution 398399

使用任务作为代码重用的机制 402406

using tasks as mechanism for code reuse 402406

{double} 参数类型 221

{double} parameter type 221

driver.findElement(By...) 调用 305

driver.findElement(By...) call 305

驱动程序对象 270

driver object 270

下拉列表 283

drop-down lists 283

DSL(领域特定语言) 310

DSL (domain-specific language) 310

动态和异步数据结构 405

dynamic and asynchronous data structures 405

E

元素 > 元素选择器 275

element > element selector 275

元素 元素选择器 275

element element selector 275

EmailMonitor 类 364

EmailMonitor class 364

已启用剧本问题类别 332

Enabled Screenplay question classes 332

确保 333334类

Ensure class 333334

确保 API 409

Ensure.that API 409

确保断言 410

Ensure.that assertions 410

确保.语句 412

Ensure.that statements 412

输入交互类 323324

Enter interaction class 323324

enterSearchCriteria() 方法 300

enterSearchCriteria() method 300

枚举值,在 339 – 340中定义角色

enum values, defining persona in 339340

史诗般的风景 110

epic landscapes 110

为每个指标寻找行动计划 106108

finding action plans for each metric 106108

寻找其他指标 108110

finding other metrics 108110

equals() 函数 412

equals() function 412

示例引导开发 18

Example-Guided Development 18

示例关键字 186

Example keyword 186

示例映射 147152

Example Mapping 147152

发现新规则 150

discovering new rules 150

主持会议 151152

facilitating sessions 151152

寻找规则和示例 149150

finding rules and examples 149150

浮现的不确定性 150151

surfacing uncertainty 150151

用户故事 148149

user stories 148149

例外条件类 287

ExceptedConditions class 287

可执行规范 13 , 204 , 209

executable specifications 13, 204, 209

自动化 5455

automating 5455

使用 Web UI 和微服务定义功能 346350

defining features using web UI and microservices 346350

探索用户旅程 347348

exploring user journey 347348

关注 UI 行为 348349

focusing on UI behavior 348349

提供全景

giving big picture 346

说明系统如何交互 349350

illustrating how systems interact 349350

对于具有 Serenity/JS 365392 的现有系统

for existing systems with Serenity/JS 365392

在规范层捕获业务上下文 387391

capturing business context in Specification layer 387391

设计可扩展的测试自动化系统 381386

designing scalable test automation systems 381386

旅程地图 366379

Journey Mapping 366379

良好的 Gherkin 场景 187203

good Gherkin scenarios 187203

坏小黄瓜 188189

bad Gherkin 188189

声明风格 189191

declarative style 189191

做好一件事 191193

doing one thing well 191193

关注本质,隐藏附带 196199

focusing on essential and hiding incidental 196199

独立 201203

independence 201203

有意义的行动者 193196

meaningful actors 193196

测试脚本与 200201

test scripts vs. 200201

组织场景 179186

organizing scenarios 179186

使用标签注释场景 182185

annotating scenarios with tags 182185

包含场景 179 – 181 的功能文件

feature files containing scenarios 179181

平面目录结构 181

flat directory structure 181

组织功能文件 181182

organizing feature files 181182

提供背景和上下文以避免重复 185186

providing background and context to avoid duplication 185186

在 Cucumber 51 – 53中录制

recording in Cucumber 5153

规则和示例 186

rules and examples 186

表 174178

tables 174178

174 – 175各步骤

in individual steps 174175

示例 175177

of examples 175177

待决情况 178

pending scenarios 178

将具体示例转化为可执行场景 186204

turning concrete examples into executable scenarios 186204

写作 2426

writing 2426

编写可执行场景 168174

writing executable scenarios 168174

And 和 But 关键字 172173

And and But keywords 172173

评论 173174

comments 173174

描述场景 169170

describing scenarios 169170

带有标题和说明的功能文件 168169

feature files with titles and descriptions 168169

预期条件类 286 , 288 , 327

ExpectedConditions class 286, 288, 327

经验 139

experience 139

显式等待 286288

Explicit waits 286288

F

F

功能覆盖范围 433

feature coverage 433

功能文件 23 , 51

feature files 23, 51

包含场景

containing scenarios

一个或多个场景 180181

one or more scenarios 180181

179 – 180概述

overview of 179180

组织 181

organizing 181

按功能和能力 182

by functionality and capability 182

按故事或产品增量 181

by stories or product increments 181

附有标题和说明 168169

with titles and descriptions 168169

功能关键字 168

Feature keyword 168

特征映射 153159

Feature Mapping 153159

将示例分解为步骤 154155

breaking examples into steps 154155

对相关流量进行分组并记录不确定性 157159

grouping related flows and recording uncertainty 157159

寻找替代流动

looking for alternate flows 156

寻找变化和新规则 155156

looking for variations and new rules 155156

从例子开始

starting with examples 154

功能就绪 430

feature readiness 430

特征 115129 , 136163

features 115129, 136163

分解成可管理的部分 121122

breaking down into manageable pieces 121122

协作方式指定 2021

collaborative approach to specifying 2021

使用 Web UI 和微服务进行定义 344350

defining using web UI and microservices 344350

可执行规范 346350

executable specifications 346350

要求 344

requirements 344

交付能力 117120

delivering capabilities 117120

描述和优先排序 4142 , 113135

describing and prioritizing 4142, 113135

故意发现 132133

deliberate discovery 132133

产品待办事项细化 114115

product backlog refinement 114115

实物期权

real options 129

发布和冲刺规划 133134

release and sprint planning 133134

使用表格 145 – 147描述复杂需求

describing complex requirements with tables 145147

发现 3940 , 4243

discovering 3940, 4243

示例映射 147152

Example Mapping 147152

发现新规则 150

discovering new rules 150

主持会议 151152

facilitating sessions 151152

寻找规则和示例 149150

finding rules and examples 149150

浮现的不确定性 150151

surfacing uncertainty 150151

用户故事 148149

user stories 148149

特征映射 153159

Feature Mapping 153159

将示例分解为步骤 154155

breaking examples into steps 154155

对相关流量进行分组并记录不确定性 157159

grouping related flows and recording uncertainty 157159

寻找替代流动

looking for alternate flows 156

寻找变化和新规则 155156

looking for variations and new rules 155156

从例子开始

starting with examples 154

关注商业价值功能 20

focus on business-value features 20

识别假设和假定,而不是 7981

identifying hypotheses and assumptions rather than 7981

举例说明 2122 , 139145

illustrating with examples 2122, 139145

旅程地图 370371

Journey Mapping 370371

错误原因 159161

OOPSI 159161

输入 161

inputs 161

成果 159

outcomes 159

产出 160

outputs 160

进程 160

process 160

情景 161

scenarios 161

愿景、能力、目标和 8183之间的关系

relationship between vision, capabilities, goals and 8183

发布功能和产品功能 127128

release features and product features 127128

报道功能覆盖范围 433435

reporting on feature coverage 433435

报告功能准备情况 431432

reporting on feature readiness 431432

需求发现研讨会 137138

requirements discovery workshops 137138

用户故事

user stories

123 – 126描述的特征

features described by 123126

功能与 126127

features vs. 126127

不符合等级制度 128129

not fitting into hierarchy 128129

切成 4445

slicing into 4445

字段值

field values

读取嵌套 357

reading nested 357

阅读顶层 357

reading top-level 357

findElement() 方法 271272 , 277 , 280 , 293

findElement() method 271272, 277, 280, 293

findElements() 方法 277 , 280

findElements() method 277, 280

findLinesThrough() 方法 6566

findLinesThrough() method 6566

findNextDepartures() 方法 5658 , 60

findNextDepartures() method 5658, 60

平面目录结构 181

flat directory structure 181

flightHistory.to JSON路径表达式 357

flightHistory.to JSONPath expression 357

{float} 参数类型 221

{float} parameter type 221

流畅接口 313

fluent interface 313

流畅等待 286288

Fluent waits 286288

表格类 398 , 422

Form class 398, 422

Form.fieldCalled(name) 方法 421

Form.fieldCalled(name) method 421

表单字段 421

form fields 421

制定阶段 4547

formulate phase 4547

常旅客自定义参数类型 224

frequentFlyer custom parameter type 224

FrequentFlyerMember 参数 223

FrequentFlyerMember parameter 223

oftenFlyerNumber 属性 356

frequentFlyerNumber attribute 356

函数参数 396

function argument 396

功能 396

functions 396

G

通用容器类 240

GenericContainer class 240

get() 方法 270

get() method 270

getAllSelectedOptions() 方法 283

getAllSelectedOptions() method 283

getAttribute() 方法 282

getAttribute() method 282

getAttributeValue() 方法 280

getAttributeValue() method 280

getCurrentUserElement() 方法 298

getCurrentUserElement() method 298

getDepartures() 方法 65

getDepartures() method 65

getEmailToken() 方法 359

getEmailToken() method 359

getFirstSelectedOption() 方法 283

getFirstSelectedOption() method 283

getInt() 方法 357

getInt() method 357

getList() 方法 357

getList() method 357

getMemberStatus() 方法 362

getMemberStatus() method 362

getObject() 方法 357

getObject() method 357

GET 查询,自动执行微服务的验收测试

GET queries, automating acceptance tests for microservices

确认地址 358359

confirming addresses 358359

确认电子邮件地址 359

confirming email addresses 359

获取一次性令牌 359

fetching single-use token 359

getText() 方法 277 , 280 , 282

getText() method 277, 280, 282

小黄瓜 17

Gherkin 17

良好情景 187203

good scenarios 187203

坏小黄瓜 188189

bad Gherkin 188189

声明风格 189191

declarative style 189191

做好一件事 191193

doing one thing well 191193

关注本质,隐藏附带 196199

focusing on essential and hiding incidental 196199

独立 201203

independence 201203

有意义的行动者 193196

meaningful actors 193196

测试脚本与 200201

test scripts vs. 200201

22 – 24概述

overview of 2224

给定关键字 400

Given keyword 400

给定语句 197 , 200

Given statement 197, 200

给定步骤 171172 , 210 , 235

Given step 171172, 210, 235

胶水代码 5567 , 219232

glue code 5567, 219232

在 @Given 方法中自动化先决条件 63

automating preconditions in @Given method 63

黄瓜表情

Cucumber expressions

自定义参数类型和 223225

custom parameter types and 223225

更加灵活 222223

making more flexible 222223

数据表

data tables

复合体 230232

complex 230232

与 229 – 230合作

working with 229230

在 @Then 方法中定义期望 5556

defining expectations in @Then method 5556

在 @When 方法中发现 Service 类 API 5657

discovering Service class API in @When method 5657

从验收测试到单元测试 57

going from acceptance tests to unit tests 57

实施服务 6465

implementing service 6465

实现 TimeTable 服务 65

implementing TimeTable service 65

使用步骤定义参数注入数据 220221

injecting data with step definition parameters 220221

列表

lists

嵌入式 228

embedded 228

简单 228

simple 228

重构完成代码 6667

refactoring completed code 6667

正则表达式 225227

regular expressions 225227

共同 227

common 227

匹配可能值集 226227

matching sets of possible values 226227

匹配特定类型的字符 226

matching specific types of characters 226

非捕获组 227

non-capturing groups 227

可选字符 227

optional characters 227

简单匹配器 226

simple matchers 226

编写简单的 TDD 测试用例 5861

writing simple TDD test case 5861

目标

goals

对企业的益处 8586

benefit to business 8586

业务流程层 251252

business flow layer 251252

深刻理解

deep shared understanding of 89

定义影响映射 94

defining for impact mapping 94

旅程地图 366368

Journey Mapping 366368

收入和 8789

revenue and 8789

写作 8687

writing 8687

H

层次任务分析(HTA) 379

Hierarchical Task Analysis (HTA) 379

HOCON 格式,将个人数据存储在 247248中

HOCON format, storing persona data in 247248

钩子 184 , 234238

hooks 184, 234238

Cucumber 事件监听器 238

Cucumber EventListeners 238

获取有关场景 235 的更多信息

getting more information about scenario 235

干预特征 237 – 238的开始和结束

intervening at start and end of feature 237238

干预前后情景 235

intervening before and after scenario 235

干预前后的具体情况 236

intervening before and after specific scenarios 236

准备测试环境

preparing test environments

内存数据库 239

in-memory databases 239

238 – 239概述

overview of 238239

HTA(层次任务分析)379

HTA (Hierarchical Task Analysis) 379

假设,确定 7981

hypotheses, identifying 7981

I

id 属性 272273 , 275 , 282

id attribute 272273, 275, 282

说明阶段 4245 , 83

illustrate phase 4245, 83

发现特征 4243

discovering features 4243

将功能分解为用户故事 4445

slicing features into user stories 4445

影响图谱 9899

impact mapping 9899

定义业务目标 94

define business goals 94

定义参与者 9496

defining actors 9496

定义可交付成果 9798

defining deliverables 9798

定义影响 9697

defining impacts 9697

识别痛点 9394

identifying pain-points 9394

反向影响映射 9899

reverse impact mapping 9899

ImplementationPendingError 396

ImplementationPendingError 396

includes() 函数 412

includes() function 412

InMemoryTimeTable 类 65 , 67

InMemoryTimeTable class 65, 67

赞美抽象(劳伦斯)319

In Praise of Abstraction (Lawrence) 319

{int} 参数类型 221

{int} parameter type 221

集成层 382 ,​​ 412425

Integration layer 382, 412425

配置 Web 集成工具 423424

configuring web integration tools 423424

识别页面元素 414416

identifying page elements 414416

实现配套页面对象 417

implementing Companion Page Objects 417

实现精益页面对象 416

implementing Lean Page Objects 416

使用页面元素 418419实现可移植的交互

implementing portable interactions with Page Elements 418419

跨项目和团队共享测试代码 424425

sharing test code across projects and teams 424425

使用页面元素查询语言描述复杂的 UI 小部件 419423

using Page Element Query Language to describe complex UI widgets 419423

为 Web 界面编写可移植测试 413414

writing portable tests for web interfaces 413414

交互类 329

Interaction class 329

相互作用 322328

interactions 322328

作为对象 325

as objects 325

与 REST API 交互 328

interacting with REST APIs 328

执行多项 323324

performing multiple 323324

执行等待以及操作 326327

performing waits as well as actions 326327

编写交互类 329330

writing interaction classes 329330

Interaction.where() 方法 329

Interaction.where() method 329

isEnabled 等待条件 327

isEnabled wait condition 327

isNotVisible() 等待条件 327

isNotVisible() wait condition 327

isPresent() 函数 412

isPresent() function 412

isPresent 等待条件 327

isPresent wait condition 327

isVisible() 等待条件 327

isVisible() wait condition 327

ItineraryService 类 56 , 71

ItineraryService class 56, 71

J

J

Java

Java

217 个 Cucumber 测试运行器类

Cucumber test runner classes in 217

Selenium WebDriver 入门 265266

getting started with Selenium WebDriver 265266

使用 JavaScript 运行 Cucumber 场景 218219

running Cucumber scenarios in JavaScript 218219

在 213 – 215中设置 Cucumber 项目

setting up Cucumber projects in 213215

旅程地图 366379

Journey Mapping 366379

将工作流与功能关联 370371

associating workflows with features 370371

确定情景的可验证后果 374377

determine verifiable consequences of scenarios 374377

确定参与者和目标 366368

determining actors and goals 366368

确定工作流程 368370

determining workflows 368370

建立情景的钢线 372374

establishing steel thread of scenarios 372374

使用任务分析来理解场景的步骤 377379

using task analysis to understand steps of scenarios 377379

旅程场景 260

journey scenarios 260

JSONPath,使用 356 – 358查询 JSON 响应

JSONPath, querying JSON responses with 356358

阅读对象集合 357358

reading collections of objects 357358

读取嵌套字段值 357

reading nested field values 357

将结构的各个部分作为对象读取 357

reading parts of structure as objects 357

读取顶级字段值 357

reading top-level field values 357

jsonPath() 方法 356

jsonPath() method 356

JSON 响应

JSON responses

部分 360362

partial 360362

使用 JSONPath 356358进行查询

querying with JSONPath 356358

阅读对象集合 357358

reading collections of objects 357358

读取嵌套字段值 357

reading nested field values 357

将结构的各个部分作为对象读取 357

reading parts of structure as objects 357

读取顶级字段值 357

reading top-level field values 357

JUnit 库 250

JUnit library 250

K

KafkaContainer 类 240

KafkaContainer class 240

科尔布,大卫 139

Kolb, David 139

大号

L

label() 方法 303

label() method 303

最后责任时刻

last responsible moment 126

劳伦斯,凯文 319

Lawrence, Kevin 319

领导者的激进管理指南,(丹宁)20

Leader’s Guide to Radical Management, The (Denning) 20

精益页面对象 416

Lean Page Objects 416

linesGoThrough() 方法 66

linesGoThrough() method 66

链接文本选择器 280

linkText selector 280

列表

lists

下拉 283

drop-down 283

嵌入式 228

embedded 228

简单 228

simple 228

生活文献 188 , 427446

living documentation 188, 427446

交付 29

delivering 29

对于遗留应用程序 445

for legacy applications 445

428 – 430的高层视图

high-level view of 428430

组织 440445

organizing 440445

按高层要求 440441

by high-level requirements 440441

发布报告 441442

for release reporting 441442

低级生活文档 443

low-level living documentation 443

单元测试作为动态文档 443445

unit tests as living documentation 443445

使用标签 441

using tags 441

产品待办事项

product backlog

整合数字产品待办事项 435438

integrating digital product backlog 435438

利用工具促进更好的协作 438439

leveraging tools for better collaboration 438439

报道功能覆盖范围 433435

reporting on feature coverage 433435

报告功能准备情况 431432

reporting on feature readiness 431432

测试为 68

tests as 68

用于支持正在进行的维护工作 3031

using to support ongoing maintenance work 3031

LocateRegistrationForm 类 399

LocateRegistrationForm class 399

登录类 310311

Login class 310311

登录页类 293294 , 298

LoginPage class 293294, 298

LoginPage 页面对象 304 , 308

LoginPage page object 304, 308

{long} 参数类型 221

{long} parameter type 221

M

马丁,珍妮 159

Martin, Jenny 159

MatchingFlightsList 页面对象 307

MatchingFlightsList page object 307

mat-form-field 元素 421

mat-form-field element 421

mat-form-field-invalid 类 301

mat-form-field-invalid class 301

mat-label 元素 421

mat-label element 421

mat-option 元素 285

mat-option element 285

mat-option 条目 285

mat-option entry 285

垫选择元素 283 , 285

mat-select element 283, 285

马茨,克里斯 129130

Matts, Chris 129130

Maven,设置项目 4751

Maven, setting up projects 4751

会员API354,356

MembershipAPI class 354, 356

会员 API 端点 353

Membership API endpoints 353

微服务 342364

microservices 342364

自动化验收测试 350363

automating acceptance tests for 350363

删除查询 362363

DELETE queries 362363

GET 查询 358359

GET queries 358359

部分 JSON 响应 360362

partial JSON responses 360362

POST 查询 353356

POST queries 353356

准备测试数据 352353

preparing test data 352353

使用 JSONPath 查询 JSON 响应 356358

querying JSON responses with JSONPath 356358

自动化更细粒度的场景并与外部服务交互 363364

automating more granular scenarios and interacting with external services 363364

使用 Web UI 定义功能和 344350

defining features using web UI and 344350

可执行规范 346350

executable specifications 346350

要求 344

requirements 344

MongoDBContainer 类 240

MongoDBContainer class 240

摩尔,杰弗里·A. 85

Moore, Geoffrey A. 85

mvn 验证命令 50

mvn verify command 50

N

名称属性 273 , 275 , 282

name attribute 273, 275, 282

导航交互类 324

Navigate interaction class 324

嵌套查找 279280

nested lookups 279280

NewFrequentFlyerEvent 事件 351

NewFrequentFlyerEvent event 351

节点[n] XPath 表达式 278

node[n] XPath expression 278

节点 XPath 表达式 278

node XPath expression 278

:nth-child(n) 选择器 276

:nth-child(n) selector 276

n 层架构 382

n-tier architecture 382

NUnit(适用于 .NET)库 250

NUnit (for .NET) library 250

O

章鱼部署 268

Octopus Deploy 268

API 422

.of API 422

OnlineCast 课程 338

OnlineCast class 338

舞台上课 341

OnStage class 341

OnStage.setTheStage() 方法 338

OnStage.setTheStage() method 338

OnStage.theActorCalled() 方法 338

OnStage.theActorCalled() method 338

OnStage.theActorInTheSpotlight() 方法 339340

OnStage.theActorInTheSpotlight() method 339340

OOPSI(结果、输出、流程、场景、输入)159161

OOPSI (Outcome, Outputs, Process, Scenarios, Inputs) 159161

输入 161

inputs 161

成果 159

outcomes 159

产出 160

outputs 160

进程 160

process 160

情景 161

scenarios 161

open() 方法 292 , 304

open() method 292, 304

开放交互类 255 , 324

Open interaction class 255, 324

Open.url() 交互 334

Open.url() interaction 334

金融期权 129

options, in finance 129

由外而内的方法 27 , 57

outside-in approach 27, 57

P

页面类别 414 , 423424

Page class 414, 423424

页面.current() API 423

Page.current() API 423

页面元素 API 419

PageElement API 419

PageElement414、423 – 424

PageElement class 414, 423424

PageElement对象415、418

PageElement objects 415, 418

页面元素查询语言 419423

Page Element Query Language 419423

页面元素

page elements

识别 414416

identifying 414416

实现可移植的交互 418419

implementing portable interactions with 418419

页面元素 API 421 , 423

PageElements API 421, 423

PageElements 类 414

PageElements class 414

PageFactory 类 304

PageFactory class 304

PageFactory.initElements() 方法 305

PageFactory.initElements() method 305

PageObject 类 308

PageObject class 308

页面对象类 316

Page Object classes 316

页面对象 293309

page objects 293309

@FindBy 注释 304306

@FindBy annotation 304306

断言 303304

assertions 303304

配套页面对象 397

Companion Page Objects 417

查找集合 306308

finding collections 306308

隐藏等待条件 302303

hiding wait conditions 302303

精益页面对象 416

Lean Page Objects 416

定位元件 293296

locating elements 293296

执行业务任务或模拟用户行为 298300

peforming business tasks or simulating user behavior 298300

以商业术语呈现状态 301302

presenting state in business terms 301302

表示对象 296

representing objects 296

宁静 BDD 308309

Serenity BDD 308309

第 296 – 298页的状态

state of page 296298

WebDriver 页面工厂和 @FindBy 注释 304306

WebDriver page factories and @FindBy annotation 304306

痛点,识别 9394

pain-points, identifying 9394

帕尔默,安迪 319

Palmer, Andy 319

参数 396

parameters 396

参数类型方法 224

ParameterType methods 224

被动语态 400

passive voice 400

PastFlight 231 舱

PastFlight class 231

PastFlight 域对象 230

PastFlight domain objects 230

人物角色(已知实体) 248

personas (known entities) 248

枚举值定义 339340

defining in enum values 339340

设计可扩展的测试自动化系统 384386

designing scalable test automation systems 384386

在场景 195196 中

in scenarios 195196

在用户故事 194195 中

in user stories 194195

以 HOCON 格式存储个人数据 247248

storing persona data in HOCON format 247248

与 246 – 247合作

working with 246247

PI(程序增量)78

PI (program increment) 78

海盗画布 101110

pirate canvases 101110

发现糟糕的事情 101105

discovering what sucks 101105

史诗般的风景 110

epic landscapes 110

为每个指标寻找行动计划 106108

finding action plans for each metric 106108

寻找其他指标 108110

finding other metrics 108110

指标

metrics

从指标到画布 101

from metrics to canvases 101

99 – 101概述

overview of 99101

PlaywrightPageElement 类 423

PlaywrightPageElement class 423

便携式测试自动化 393426

portable test automation 393426

设计领域层 394412

designing Domain layer 394412

将交互组合成任务 397398

composing interactions into tasks 397398

实施业务领域任务 396397

implementing business domain tasks 396397

执行验证任务 407412

implementing verification tasks 407412

利用混合测试实现非 UI 交互 399401

leveraging non-UI interactions with blended testing 399401

建模业务领域任务 395396

modeling business domain tasks 395396

由外而内的方法实现任务替代 398399

outside-in approach to enable task substitution 398399

使用任务作为代码重用的机制 402406

using tasks as mechanism for code reuse 402406

设计便携式集成层 412425

designing portable Integration layer 412425

配置 Web 集成工具 423424

configuring web integration tools 423424

识别页面元素 414416

identifying page elements 414416

实现配套页面对象 417

implementing Companion Page Objects 417

实现精益页面对象 416

implementing Lean Page Objects 416

使用页面元素 418419实现可移植的交互

implementing portable interactions with Page Elements 418419

跨项目和团队共享测试代码 424425

sharing test code across projects and teams 424425

使用页面元素查询语言描述复杂的 UI 小部件 419423

using Page Element Query Language to describe complex UI widgets 419423

为 Web 界面编写可移植测试 413414

writing portable tests for web interfaces 413414

PostgreSQL容器类 240

PostgreSQLContainer class 240

POST 查询,自动执行微服务验收测试 353356

POST queries, automating acceptance tests for microservices 353356

产品待办事项

product backlog

整合数字产品待办事项 435438

integrating digital product backlog 435438

利用工具促进更好的协作 438439

leveraging tools for better collaboration 438439

114 – 115的精炼

refinement of 114115

产品特点 127128

product features 127128

承诺 235

Promises 235

{代词} 自定义参数类型 390

{pronoun} custom parameter type 390

{代词} 标记 390

{pronoun} tokens 390

提议出发变量 56

proposedDepartures variable 56

Q

问题适配器 API 406

QuestionAdapter API 406

QuestionAdapter 代理对象 405

QuestionAdapter proxy object 405

问题类别 330333

Question classes 330333

Question.fromObject API 405

Question.fromObject API 405

问题 330334

questions 330334

特定领域问题类别 333

domain-specific Question classes 333

查询系统状态 331332

querying state of system 331332

用来做出断言 333334

using to make assertions 333334

R

R

单选按钮 282283

radio buttons 282283

实物期权

real options 129

尽早承诺

committing early 132

到期 131132

expiring 131132

具有价值 130131

having value 130131

重构完成代码 6667

refactoring completed code 6667

推荐 100

referral 100

反思 139

reflection 139

register() 方法 354

register() method 354

正则表达式 225227

regular expressions 225227

共同 227

common 227

匹配可能值集 226227

matching sets of possible values 226227

匹配特定类型的字符 226

matching specific types of characters 226

非捕获组 227

non-capturing groups 227

可选字符 227

optional characters 227

简单匹配器 226

simple matchers 226

发布和冲刺规划 133134

release and sprint planning 133134

公布证据

released evidence 428

发布功能 127128

release features 127128

发布报告,组织 441442的动态文档

release reporting, organizing living documentation for 441442

需求分析 1718

requirements analysis 1718

需求发现研讨会 137138

requirements discovery workshops 137138

响应对象 355

Response object 355

REST API,与 328 交互

REST APIs, interactions interacting with 328

RestAssured.baseURI 属性 354

RestAssured.baseURI property 354

保留 100

retention 100

返回 100

return 100

反向影响映射 9899

reverse impact mapping 9899

规则关键字 186

Rule keyword 186

年代

S

SBE(示例说明) 18

SBE (specification by example) 18

场景关键字 169 , 186

Scenario keyword 169, 186

场景大纲关键字 177

Scenario Outline keyword 177

情景 45 , 134

scenarios 45, 134

好 187203

good 187203

坏小黄瓜 188189

bad Gherkin 188189

声明风格 189191

declarative style 189191

做好一件事 191193

doing one thing well 191193

关注本质,隐藏附带 196199

focusing on essential and hiding incidental 196199

独立 201203

independence 201203

有意义的行动者 193196

meaningful actors 193196

测试脚本与 200201

test scripts vs. 200201

旅程地图

Journey Mapping

确定可验证的后果 374377

determine verifiable consequences 374377

建立 372374

establishing 372374

使用任务分析来理解步骤 377379

using task analysis to understand steps 377379

161 号

OOPSI 161

组织 179186

organizing 179186

使用标签注释场景 182185

annotating scenarios with tags 182185

背景和上下文,以避免重复 185186

background and context to avoid duplication 185186

包含场景 179 – 181 的功能文件

feature files containing scenarios 179181

平面目录结构 181

flat directory structure 181

组织功能文件 181182

organizing feature files 181182

将具体示例转化为可执行文件 187204

turning concrete examples into executable 187204

使用 174 – 178中的表格

using tables in 174178

174 – 175各步骤

in individual steps 174175

待决情况 178

pending scenarios 178

示例表 175177

tables of examples 175177

编写可执行文件 168174

writing executable 168174

And 和 But 关键字 172173

And and But keywords 172173

评论 173174

comments 173174

描述场景 169170

describing scenarios 169170

带有标题和说明的功能文件 168169

feature files with titles and descriptions 168169

场景关键字 177

Scenarios keyword 177

ScheduledService 类 67

ScheduledService class 67

scheduleService 方法 63

scheduleService method 63

剧本-jvm 319

screenplay-jvm 319

剧本模式 254 , 315341

Screenplay Pattern 254, 315341

演员

actors

能力 328329

abilities 328329

定义 320321

defined 320321

相互作用 322328

interactions 322328

问题 330334

questions 330334

任务 321322

tasks 321322

黄瓜和337341

Cucumber and 337341

演员和演员阵容 338

actors and casts 338

为参与者定义自定义参数类型 339

defining custom parameter type for actors 339

在枚举值中定义角色 339340

defining persona in enum values 339340

Cucumber 340 – 341中的剧本断言

Screenplay assertions in Cucumber 340341

剧本阶段 338339

Screenplay stage 338339

定义 316319

defined 316319

交互类 329330

interaction classes 329330

需要 316319

need for 316319

概述

overview of 320

任务 334337

tasks 334337

增强可重用性 335337

enhancing reusability 335337

提高可读性 334335

improving readability 334335

滚动交互类 324

Scroll interaction class 324

SDS(​​系统设计规范) 29

SDS (system design specification) 29

搜索航班行动类 317

SearchFlights action class 317

搜索航班 317318舱

SearchFlights class 317318

SearchForm 类 299 , 301 , 303

SearchForm class 299, 301, 303

SearchForm 页面对象 300

SearchForm page object 300

selectByIndex() 方法 283

selectByIndex() method 283

selectByValue() 方法 283

selectByValue() method 283

selectByVisibleText() 方法 283

selectByVisibleText() method 283

SelectedValue 剧本问题类别 332

SelectedValue Screenplay question classes 332

SelectFromOptions 交互类 324

SelectFromOptions interaction class 324

选择交互类 283 , 324

Select interaction class 283, 324

Selenium WebDriver 258

Selenium WebDriver 258

自动化验收标准 263265

automating acceptance criteria 263265

Java 入门 265266

getting started with in Java 265266

与 Cucumber 集成 269

integrating with Cucumber 269

页面工厂 304306

page factories 304306

设置 267268

setting up 267268

在步骤定义类之间共享实例 269

sharing instances between step definition classes 269

sendKeys() 方法 271272 , 280 , 282 , 308 , 323

sendKeys() method 271272, 280, 282, 308, 323

宁静/JS 365392

Serenity/JS 365392

在规范层捕获业务上下文 387391

capturing business context in Specification layer 387391

设计领域层 394412

designing Domain layer 394412

将交互组合成任务 397398

composing interactions into tasks 397398

实施业务领域任务 396397

implementing business domain tasks 396397

执行验证任务 407412

implementing verification tasks 407412

利用混合测试实现非 UI 交互 399401

leveraging non-UI interactions with blended testing 399401

建模业务领域任务 395396

modeling business domain tasks 395396

由外而内的方法实现任务替代 398399

outside-in approach to enable task substitution 398399

使用任务作为代码重用的机制 402406

using tasks as mechanism for code reuse 402406

设计便携式集成层 412425

designing portable Integration layer 412425

配置 Web 集成工具 423424

configuring web integration tools 423424

识别页面元素 414416

identifying page elements 414416

实现配套页面对象 417

implementing Companion Page Objects 417

实现精益页面对象 416

implementing Lean Page Objects 416

使用页面元素 418419实现可移植的交互

implementing portable interactions with Page Elements 418419

跨项目和团队共享测试代码 424425

sharing test code across projects and teams 424425

使用页面元素查询语言描述复杂的 UI 小部件 419423

using Page Element Query Language to describe complex UI widgets 419423

为 Web 界面编写可移植测试 413414

writing portable tests for web interfaces 413414

设计可扩展的测试自动化系统 381386

designing scalable test automation systems 381386

分层架构 382383

layered architecture 382383

使用演员来描述角色 384386

using actors to describe personas 384386

使用参与者链接各层 383384

using actors to link layers 383384

旅程地图 366379

Journey Mapping 366379

将工作流与功能关联 370371

associating workflows with features 370371

确定情景的可验证后果 374377

determine verifiable consequences of scenarios 374377

确定参与者和目标 366368

determining actors and goals 366368

确定工作流程 368370

determining workflows 368370

建立情景的钢线 372374

establishing steel thread of scenarios 372374

使用任务分析来理解场景的步骤 377379

using task analysis to understand steps of scenarios 377379

Serenity/JS 断言库 409

Serenity/JS Assertions library 409

宁静 BDD 308309

Serenity BDD 308309

服务类 API,在@When 方法中发现 5657

Service class API, discovering in @When method 5657

setEventPublisher() 方法 238

setEventPublisher() method 238

{short} 参数类型 221

{short} parameter type 221

signin-WithCredentials() 方法 291

signin-WithCredentials() method 291

signinWithCredentials() 方法 299

signinWithCredentials() method 299

注册任务 401

SignUp task 401

SignUp.using 静态工厂方法 389

SignUp.using static factory method 389

孤立的开发方法 33

siloed development approach 33

SMART(具体、可衡量、可实现、相关、有时限) 87

SMART (Specific Measurable Achievable Relevant Time-bound) 87

SOA(面向服务架构) 344

SOA (service-oriented architectures) 344

实例说明(Adzic) 18

Specification by Example (Adzic) 18

实例规范 (SBE) 18

specification by example (SBE) 18

规范层 382 ,​​ 387391

Specification layer 382, 387391

推测阶段 3642 , 75

speculate phase 3642, 75

描述特征 4142

describing features 4142

发现功能和特点 3940

discovering capabilities and features 3940

目标

goals

对企业的益处 8586

benefit to business 8586

深刻理解

deep shared understanding of 89

愿景、能力、特征和 8183之间的关系

relationship between vision, capabilities, features and 8183

收入和 8789

revenue and 8789

写作 8687

writing 8687

假设和假定 7981

hypotheses and assumptions 7981

确定业务目标 3839

identifying business objectives 3839

影响图谱 9899

impact mapping 9899

定义业务目标 94

define business goals 94

定义参与者 9496

defining actors 9496

定义可交付成果 9798

defining deliverables 9798

定义影响 9697

defining impacts 9697

识别痛点 9394

identifying pain-points 9394

反向影响映射 9899

reverse impact mapping 9899

海盗画布 101110

pirate canvases 101110

建造史诗般的景观 110

building epic landscapes 110

发现糟糕的事情 101105

discovering what sucks 101105

从指标到画布 101

from metrics to canvases 101

指标 99101

metrics 99101

战略规划

strategic planning

持续活动

continuous activity 78

在项目中 77

in projects 77

让利益相关者和团队成员参与 7879

involving stakeholders and team members 7879

想象

vision

目标、能力、特征之间的关系以及 8183

relationship between goals, capabilities, features and 8183

从 8384开始

starting with 8384

愿景陈述 84

vision statements 84

愿景陈述模板 8485

vision statement templates 8485

SpringBoot,将TestContainers与241242集成

SpringBoot, integrating TestContainers with 241242

利益攸关方

stakeholders 94

静态和同步数据结构 405

static and synchronous data structure 405

步骤定义 210212

step definitions 210212

使用步骤定义参数注入数据 220221

injecting data with step definition parameters 220221

在步骤定义类之间共享实例 269

sharing instances between step definition classes 269

stepdefinitions 包 215

stepdefinitions package 215

故事测试驱动开发 18

Story Test-Driven Development 18

战略规划

strategic planning

持续活动

continuous activity 78

在项目中 77

in projects 77

让利益相关者和团队成员参与 7879

involving stakeholders and team members 7879

战略规划活动

strategic planning activities 134

{string} 参数类型 221

{string} parameter type 221

submitSearch() 方法 303

submitSearch() method 303

switchTo() 方法 330

switchTo() method 330

系统设计规范(SDS) 29

system design specification (SDS) 29

电视

T

tables

用 145 – 147描述复杂需求

describing complex requirements with 145147

在场景 174 – 178中使用

using in scenarios 174178

174 – 175各步骤

in individual steps 174175

示例表 175177

tables of examples 175177

标签

tags

使用 182 – 185注释场景

annotating scenarios with 182185

组织生活文档 441

organizing living documentation 441

标签选择器 275

tag selector 275

记笔记 386 , 404

TakeNotes 386, 404

任务板 435

task board 435

任务对象 396

Task object 396

任务321322、334 337​

tasks 321322, 334337

业务领域任务

business domain tasks

实施 396397

implementing 396397

建模 395396

modeling 395396

将交互组合成 397398

composing interactions into 397398

增强可重用性 335337

enhancing reusability 335337

执行验证任务 407412

implementing verification tasks 407412

提高可读性 334335

improving readability 334335

由外而内的方法实现任务替代 398399

outside-in approach to enable task substitution 398399

使用代码重用机制 402406

using as mechanism for code reuse 402406

任务构建 334

tasks construct 334

Task.where() 方法 334

Task.where() method 334

任务.where API 396

Task.where API 396

TDD(测试驱动开发) 443

TDD (Test-Driven Development) 443

BDD 的起源 1517

origin of BDD 1517

编写简单的测试用例 5861

writing simple test case 5861

技术层 254255

technical layer 254255

TestContainer 对象 240

TestContainer object 240

测试容器 240242

TestContainers 240242

依赖项 240

dependencies 240

将 TestContainers 与 Cucumber 和 SpringBoot 集成 241242

integrating TestContainers with Cucumber and SpringBoot 241242

起始实例 240241

starting instance 240241

测试有效学习过程 139

test effective learning process 139

TestNG(适用于 Java)库 250

TestNG (for Java) library 250

文本类 332333

Text class 332333

文本字段

text fields

读取 282 的值

reading values from 282

与 282 合作

working with 282

then() 方法 360

then() method 360

然后语句 200

Then statement 200

然后步骤172,235

Then step 172, 235

ThreadLocal 类 241

ThreadLocal class 241

ThreadLocal 对象 269

ThreadLocal object 269

三个朋友的方法 137138

Three Amigos approach 137138

时间表 API 59

TimeTable API 59

时间表类 6061 , 64

TimeTable class 6061, 64

时间表接口57、61、63 – 64

TimeTable interface 57, 61, 6364

时间表服务,实施 65

TimeTable service, implementing 65

祝酒 407

toast 407

Toaster.message 公共方法 412

Toaster.message public method 412

Toaster.status 公共方法 412

Toaster.status public method 412

toast-message 网络元素 415

toast-message web element 415

Toastr 库 286

Toastr library 286

TokenAPI 类 359

TokenAPI class 359

toTheLoginPage() 方法 336

toTheLoginPage() method 336

transfer() 方法 16

transfer() method 16

旅行者详情类 387 , 404

TravelerDetails class 387, 404

TravelerDetails.of(actorName) 静态方法 387

TravelerDetails.of(actorName) static method 387

travelerDetails 变量 390

travelerDetails variable 390

旅行者舱 281

Traveller class 281

TravellerPersonas 类 248

TravellerPersonas class 248

type() 方法 308

type() method 308

类型安全配置 API 248

Typesafe config API 248

Typesafe 配置库 247

Typesafe config library 247

TypeScript

TypeScript

在 218 – 219中运行 Cucumber 场景

running Cucumber scenarios in 218219

在213、215设置 Cucumber 项目

setting up Cucumber projects in 213, 215

U

不确定

uncertainty

拥抱21

embracing 21

知识限制 1112

knowledge constraint 1112

录音 157159

recording 157159

表面处理 150151

surfacing 150151

单元测试

unit tests

BDD 原则与实践 2628

BDD principles and practices 2628

从验收测试到 57

going from acceptance tests to 57

用户旅程

user journeys

业务流程层 251252

business flow layer 251252

可执行规范探索 347348

executable specifications that explore 347348

说明 260261

illustrating 260261

用户故事

user stories

示例映射 148149

Example Mapping 148149

123 – 126描述的特征

features described by 123126

功能与 126127

features vs. 126127

不符合等级制度 128129

not fitting into hierarchy 128129

按 181 整理功能文件

organizing feature files by by 181

194 – 195中的人物

personas in 194195

将特征分为 4445

slicing features into 4445

UX(用户体验 193、246

UX (user experience) 193, 246

V

验证阶段 36

Validate phase 36

价值剧本问题类 332

Value Screenplay question classes 332

虚拟测试环境 239242

virtual test environments 239242

能见度等级 331332

Visibility class 331332

visibilityOfElementLocated() 方法 287

visibilityOfElementLocated() method 287

想象

vision

目标、能力、特征之间的关系以及 8183

relationship between goals, capabilities, features and 8183

从 8384开始

starting with 8384

愿景陈述 84

vision statements 84

愿景陈述模板 8485

vision statement templates 8485

西

W

等待班级327

Wait class 327

等待条件 287

Wait conditions 287

Wait.for(duration) 同步语句 412

Wait.for(duration) synchronization statements 412

等待对象 288

wait object 288

等待

waits

显式等待 286288

Explicit waits 286288

流畅等待 286288

Fluent waits 286288

隐藏条件 302303

hiding conditions 302303

执行等待和操作的交互 326327

interactions performing waits as well as actions 326327

等待 API 409

Wait.until API 409

WaitUntil 交互 326

WaitUntil interaction 326

Wait.until 语句 410 , 412

Wait.until statements 410, 412

WebDriver 类 270 , 298 , 325

WebDriver class 270, 298, 325

WebDriver 实例 267270 , 280 , 292 , 329 , 338

WebDriver instance 267270, 280, 292, 329, 338

WebdriverIOPage 类 423

WebdriverIOPage class 423

WebDriverWait 类 286

WebDriverWait class 286

WebDriverWait 方法 287

WebDriverWait methods 287

WebElement 类 271272 , 298 , 308

WebElement class 271272, 298, 308

WebElementFacade 类 308

WebElementFacade class 308

WebElement 实例 280

WebElement instances 280

WebElement 对象 271

WebElement object 271

网页元素

web elements

与 280283交互

interacting with 280283

复选框 282283

check boxes 282283

下拉列表 283

drop-down lists 283

准备测试数据 280282

preparing test data 280282

单选按钮 282283

radio buttons 282283

文本字段 282

text fields 282

定位 272280

locating 272280

通过 ID 或名称识别 273

identifying by ID or name 273

通过链接文本识别 274275

identifying by link text 274275

使用 CSS 275277进行识别

identifying using CSS 275277

使用数据属性识别 273

identifying using data attributes 273

使用 XPath 识别 277279

identifying using XPath 277279

嵌套查找 279280

nested lookups 279280

页面对象 293296

page objects 293296

WebElementStateMatchers 类 327

WebElementStateMatchers class 327

当关键字 400

When keyword 400

当语句 197 , 200

When statement 197, 200

步骤172、210、235

When step 172, 210, 235

{word} 参数类型 221

{word} parameter type 221

X

XPath,使用 277 – 279识别 Web 元素

XPath, identifying web elements using 277279

封底内页

inside back cover

Screenplay 模式是一个以演员为中心的模型,它帮助我们从与系统交互的演员的角度来表达测试场景,并使用我们的领域语言。

The Screenplay Pattern is an actor-centric model, helping us express test scenarios from the perspective of an actor interacting with the system, and using our domain language.